import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { action, computed, makeObservable, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { lift, toggleMark, wrapIn } from 'prosemirror-commands'; import { Mark, MarkType } from 'prosemirror-model'; import { wrapInList } from 'prosemirror-schema-list'; import { EditorState, NodeSelection, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import * as React from 'react'; import { Doc } from '../../../../fields/Doc'; import { BoolCast, Cast, StrCast } from '../../../../fields/Types'; import { DocServer } from '../../../DocServer'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { DocumentView } from '../DocumentView'; import { EquationBox } from '../EquationBox'; import { FieldViewProps } from '../FieldView'; import { FormattedTextBox, FormattedTextBoxProps } from './FormattedTextBox'; import { updateBullets } from './ProsemirrorExampleTransfer'; import './RichTextMenu.scss'; import { schema } from './schema_rts'; @observer export class RichTextMenu extends AntimodeMenu { // eslint-disable-next-line no-use-before-define static _instance: { menu: RichTextMenu | undefined } = observable({ menu: undefined }); static get Instance() { return RichTextMenu._instance?.menu; } public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable private _linkToRef = React.createRef(); dataDoc: Doc | undefined; @observable public view?: EditorView & { TextView?: FormattedTextBox } = undefined; public editorProps: FieldViewProps | AntimodeMenuProps | undefined; public _brushMap: Map> = new Map(); @observable private collapsed: boolean = false; @observable private _noLinkActive: boolean = false; @observable private _boldActive: boolean = false; @observable private _italicActive: boolean = false; @observable private _underlineActive: boolean = false; @observable private _strikethroughActive: boolean = false; @observable private _subscriptActive: boolean = false; @observable private _superscriptActive: boolean = false; @observable private _activeFontSize: string = '13px'; @observable private _activeFontFamily: string = ''; @observable private _activeFitBox: boolean = false; @observable private _activeListType: string = ''; @observable private _activeAlignment: string = 'left'; @observable private brushMarks: Set = new Set(); @observable private showBrushDropdown: boolean = false; @observable private _activeFontColor: string = 'black'; @observable private showColorDropdown: boolean = false; @observable private _activeHighlightColor: string = 'transparent'; @observable private showHighlightDropdown: boolean = false; @observable private currentLink: string | undefined = ''; @observable private showLinkDropdown: boolean = false; constructor(props: AntimodeMenuProps) { super(props); makeObservable(this); runInAction(() => { RichTextMenu._instance.menu = this; this.updateMenu(undefined, undefined, props, this.dataDoc); this._canFade = false; this.Pinned = true; }); } @computed get RootSelected() { return this.TextView?._props.rootSelected?.() || this.TextView?._props.isContentActive(); } @computed get noAutoLink() { return this._noLinkActive; } @computed get bold() { return this._boldActive; } @computed get underline() { return this._underlineActive; } @computed get italic() { return this._italicActive; } @computed get strikeThrough() { return this._strikethroughActive; } @computed get fontColor() { return this._activeFontColor; } @computed get fontHighlight() { return this._activeHighlightColor; } @computed get fitBox() { return this._activeFitBox; } @computed get fontFamily() { return this._activeFontFamily; } @computed get fontSize() { return this._activeFontSize; } @computed get listStyle() { return this._activeListType; } @computed get textAlign() { return this._activeAlignment; } @computed get textVcenter() { return BoolCast(this.dataDoc?.text_centered, BoolCast(Doc.UserDoc().textCentered)); } @action public updateMenu(view: EditorView | undefined, lastState: EditorState | undefined, props: FormattedTextBoxProps | AntimodeMenuProps | undefined, dataDoc: Doc | undefined) { if (this._linkToRef.current?.getBoundingClientRect().width) { return; } this.view = view; this.dataDoc = dataDoc; props && (this.editorProps = props); // Don't do anything if the document/selection didn't change if (view && view.hasFocus()) { if (lastState?.doc.eq(view.state.doc) && lastState.selection.eq(view.state.selection)) return; } this.setActiveMarkButtons(this.getActiveMarksOnSelection()); const active = this.getActiveFontStylesOnSelection(); const { activeFamilies } = active; const { activeSizes } = active; const { activeColors } = active; const { activeHighlights } = active; const refDoc = DocumentView.Selected().lastElement()?.dataDoc ?? Doc.UserDoc(); const refField = (pfx => (pfx ? pfx + '_' : ''))(DocumentView.Selected().lastElement()?.LayoutFieldKey); const refVal = (field: string, dflt: string) => StrCast(refDoc[refField + field], StrCast(Doc.UserDoc()[field], dflt)); this._activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); this._activeFitBox = BoolCast(refDoc[refField + 'fitBox'], BoolCast(Doc.UserDoc().fitBox)); this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontFamily'], refVal('fontFamily', 'Arial'))) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(this.dataDoc?.[Doc.LayoutDataKey(this.dataDoc) + '_fontSize'], refVal('fontSize', '10px'))) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, refVal('fontColor', 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; // update link in current selection this.getTextLinkTargetTitle().then(targetTitle => this.setCurrentLink(targetTitle)); } setMark = (mark: Mark, state: EditorState, dispatch: (tr: Transaction) => void, dontToggle: boolean = false) => { if (mark) { const newPos = state.selection.$anchor.node()?.type === schema.nodes.ordered_list ? state.selection.from : state.selection.from; const node = (state.selection as NodeSelection).node ?? (newPos >= 0 ? state.doc.nodeAt(newPos) : undefined); if (node?.type === schema.nodes.ordered_list || node?.type === schema.nodes.list_item) { const hasMark = node.marks.some(m => m.type === mark.type); const otherMarks = node.marks.filter(m => m.type !== mark.type); const addAnyway = node.marks.filter(m => m.type === mark.type && Object.keys(m.attrs).some(akey => m.attrs[akey] !== mark.attrs[akey])); const markup = state.tr.setNodeMarkup(newPos, node.type, node.attrs, hasMark && !addAnyway ? otherMarks : [...otherMarks, mark]); dispatch(updateBullets(markup, state.schema)); } else if (state) { const { tr } = state; if (dontToggle) { tr.addMark(state.selection.from, state.selection.to, mark); dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(state.selection.from), tr.doc.resolve(state.selection.to)))); // bcz: need to redo the selection because ctrl-a selections disappear otherwise } else { toggleMark(mark.type, mark.attrs)(state, dispatch); } } } this.setActiveMarkButtons(this.getActiveMarksOnSelection()); }; // finds font sizes and families in selection getActiveAlignment = () => { if (this.view && this.RootSelected) { const from = this.view.state.selection.$from; for (let i = from.depth; i >= 0; i--) { const node = from.node(i); if (node.type === this.view.state.schema.nodes.paragraph || node.type === this.view.state.schema.nodes.heading) { return node.attrs.align || 'left'; } } } else if (this.dataDoc) { return StrCast(this.dataDoc.text_align) || 'left'; } return StrCast(Doc.UserDoc().textAlign) || 'left'; }; // finds font sizes and families in selection getActiveListStyle = () => { const state = this.view?.state; if (state) { const pos = state.selection.$anchor; for (let i = 0; i < pos.depth; i++) { const node = pos.node(i); if (node.type === schema.nodes.ordered_list) { return node.attrs.mapStyle; } } } return ''; }; // finds font sizes and families in selection getActiveFontStylesOnSelection() { const activeFamilies = new Set(); const activeSizes = new Set(); const activeColors = new Set(); const activeHighlights = new Set(); if (this.view && this.RootSelected) { const { state } = this.view; const pos = this.view.state.selection.$from; let marks: Mark[] = [...(state.storedMarks ?? [])]; if (state.storedMarks !== null) { /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } marks.forEach(m => { m.type === state.schema.marks.pFontFamily && activeFamilies.add(m.attrs.fontFamily); m.type === state.schema.marks.pFontColor && activeColors.add(m.attrs.fontColor); m.type === state.schema.marks.pFontSize && activeSizes.add(m.attrs.fontSize); m.type === state.schema.marks.pFontHighlight && activeHighlights.add(String(m.attrs.fontHighlight)); }); } else if (DocumentView.Selected().some(dv => dv.ComponentView instanceof EquationBox)) { DocumentView.Selected().forEach(dv => StrCast(dv.Document._text_fontSize) && activeSizes.add(StrCast(dv.Document._text_fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } getMarksInSelection(state: EditorState) { const found = new Set(); const { from, to } = state.selection as TextSelection; state.doc.nodesBetween(from, to, node => node.marks.forEach(m => found.add(m))); return found; } // finds all active marks on selection in given group getActiveMarksOnSelection() { if (!this.view || !this.RootSelected) return [] as MarkType[]; const { state } = this.view; let marks: Mark[] = [...(state.storedMarks ?? [])]; const pos = this.view.state.selection.$from; if (state.storedMarks !== null) { /* empty */ } else if (state.selection.empty) { for (let i = 0; i <= pos.depth; i++) { marks = [...Array.from(pos.node(i).marks), ...this.view.state.selection.$anchor.marks(), ...marks]; } } else { state.doc.nodesBetween(state.selection.from, state.selection.to, (node /* , pos, parent, index */) => { node.marks?.filter(mark => !mark.isInSet(marks)).map(mark => marks.push(mark)); }); } const markGroup = [schema.marks.noAutoLinkAnchor, schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript]; return markGroup.filter(markType => { const mark = state.schema.mark(markType); return mark.isInSet(marks); }); } @action setActiveMarkButtons(activeMarks: MarkType[] | undefined) { if (!activeMarks) return; this._noLinkActive = false; this._boldActive = false; this._italicActive = false; this._underlineActive = false; this._strikethroughActive = false; this._subscriptActive = false; this._superscriptActive = false; activeMarks.forEach(mark => { switch (mark.name) { case 'noAutoLinkAnchor': this._noLinkActive = true; break; case 'strong': this._boldActive = true; break; case 'em': this._italicActive = true; break; case 'underline': this._underlineActive = true; break; case 'strikethrough': this._strikethroughActive = true; break; case 'subscript': this._subscriptActive = true; break; case 'superscript': this._superscriptActive = true; break; default: } // prettier-ignore }); } elideSelection = (txstate: EditorState | undefined = undefined, visibility = false) => { const state = txstate ?? this.view?.state; if (!state || state.selection.empty) return false; const mark = state.schema.marks.summarize.create(); const tr = state.tr.addMark(state.tr.selection.from, state.selection.to, mark); const text = tr.selection.content(); const elideNode = state.schema.nodes.summary.create({ visibility, text, textslice: text.toJSON() }); const summary = tr.replaceSelectionWith(elideNode).removeMark(tr.selection.from - 1, tr.selection.from, mark); const expanded = () => { const endOfElidableText = summary.selection.to + text.content.size; const res = summary.insert(summary.selection.to, text.content).insert(endOfElidableText, state.schema.nodes.paragraph.create({})); return res.setSelection(new TextSelection(res.doc.resolve(endOfElidableText + 1))); }; this.view?.dispatch?.(visibility ? expanded() : summary); return true; }; toggleNoAutoLinkAnchor = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.noAutoLinkAnchor); this.setMark(mark, this.view.state, this.view.dispatch, false); this.TextView?.autoLink(); this.view.focus(); } }; toggleFitBox = () => { if (this.dataDoc) { const doc = this.dataDoc; (document.activeElement as HTMLElement)?.blur(); doc.text_fitBox = !doc.text_fitBox; } else { Doc.UserDoc().fitBox = !Doc.UserDoc().fitBox; Doc.UserDoc().textAlign = Doc.UserDoc().fitBox ? 'center' : undefined; } this.updateMenu(undefined, undefined, undefined, this.dataDoc); }; toggleBold = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.strong); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; toggleUnderline = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.underline); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; toggleItalic = () => { if (this.view) { const mark = this.view.state.schema.mark(this.view.state.schema.marks.em); this.setMark(mark, this.view.state, this.view.dispatch, false); this.view.focus(); } }; setFontField = (value: string, fontField: 'fitBox' | 'fontSize' | 'fontFamily' | 'fontColor' | 'fontHighlight') => { if (this.TextView && this.view && fontField !== 'fitBox') { const anchorNode = window.getSelection()?.anchorNode; if (this.view.hasFocus() || (anchorNode && this.TextView.ProseRef?.contains(anchorNode))) { const attrs: { [key: string]: string } = {}; attrs[fontField] = value; const fmark = this.view.state.schema.marks['pF' + fontField.substring(1)].create(attrs); this.setMark(fmark, this.view.state, (tx: Transaction) => this.view?.dispatch(tx.addStoredMark(fmark)), true); } else { Array.from(new Set([...DocumentView.Selected(), this.TextView.DocumentView?.()])) .filter(v => v?.ComponentView instanceof FormattedTextBox && v.ComponentView.EditorView?.TextView) .map(v => v!.ComponentView as FormattedTextBox) .forEach(view => { view.EditorView!.TextView!.dataDoc[(view.EditorView!.TextView!.fieldKey ?? 'text') + `_${fontField}`] = value; }); } } else if (this.dataDoc) { this.dataDoc[`${Doc.LayoutDataKey(this.dataDoc)}_${fontField}`] = value; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } else { Doc.UserDoc()[fontField] = value; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; // TODO: remove doesn't work // remove all node type and apply the passed-in one to the selected text changeListType = (mapStyle: string) => { const active = this.view?.state && RichTextMenu.Instance?.getActiveListStyle(); const newMapStyle = active === mapStyle ? '' : mapStyle; if (!this.view || newMapStyle === '') return; const inList = this.view.state.selection.$anchor.node(1).type === schema.nodes.ordered_list; const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks()); if (inList) { const tx2 = updateBullets(this.view.state.tr, schema, newMapStyle, this.view.state.doc.resolve(this.view.state.selection.$anchor.before(1) + 1).pos, this.view.state.doc.resolve(this.view.state.selection.$anchor.after(1)).pos); marks && tx2.ensureMarks([...marks]); marks && tx2.setStoredMarks([...marks]); this.view.dispatch(tx2); } else !wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: Transaction) => { const tx3 = updateBullets(tx2, schema, newMapStyle, this.view!.state.selection.from - 1, this.view!.state.selection.to + 1); marks && tx3.ensureMarks([...marks]); marks && tx3.setStoredMarks([...marks]); this.view!.dispatch(tx3); }); this.view.focus(); }; vcenterToggle = () => { if (this.dataDoc) this.dataDoc.text_centered = !this.dataDoc.text_centered; else Doc.UserDoc().textCentered = !Doc.UserDoc().textCentered; }; align = (view: EditorView | undefined, dispatch: undefined | ((tr: Transaction) => void), alignment: 'left' | 'right' | 'center') => { if (view && dispatch && this.RootSelected) { let { tr } = view.state; view.state.doc.nodesBetween(view.state.selection.from, view.state.selection.to, (node, pos) => { if ([schema.nodes.paragraph, schema.nodes.heading].includes(node.type)) { tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, align: alignment }, node.marks); return false; } view.focus(); return true; }); view.focus(); dispatch?.(tr); } else { if (this.dataDoc) { this.dataDoc.text_align = alignment; } else Doc.UserDoc().textAlign = alignment; this.updateMenu(undefined, undefined, undefined, this.dataDoc); } }; paragraphSetup(state: EditorState, dispatch: (tr: Transaction) => void, field: 'inset' | 'indent', value?: 0 | 10 | -10) { let { tr } = state; state.doc.nodesBetween(state.selection.from, state.selection.to, (node, pos) => { if (node.type === schema.nodes.paragraph || node.type === schema.nodes.heading) { const newValue = !value ? (node.attrs[field] ? 0 : node.attrs[field] + 10) : Math.max(0, value); // prettier-ignore tr = tr.setNodeMarkup(pos, node.type, { ...node.attrs, ...(field === 'inset' ? { inset: newValue } : { indent: newValue }) }, node.marks); return false; } return true; }); dispatch?.(tr); return true; } insertBlockquote(state: EditorState, dispatch: (tr: Transaction) => void) { const node = state.selection.$from.depth ? state.selection.$from.node(state.selection.$from.depth - 1) : undefined; if (node?.type === schema.nodes.blockquote) { lift(state, dispatch); } else { wrapIn(schema.nodes.blockquote)(state, dispatch); } return true; } insertHorizontalRule(state: EditorState, dispatch: (tr: Transaction) => void) { dispatch(state.tr.replaceSelectionWith(state.schema.nodes.horizontal_rule.create()).scrollIntoView()); return true; } @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; } // todo: add brushes to brushMap to save with a style name onBrushNameKeyPress = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { RichTextMenu.Instance?.brushMarks && RichTextMenu.Instance?._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks); this._brushNameRef.current!.style.background = 'lightGray'; } }; _brushNameRef = React.createRef(); @action clearBrush() { RichTextMenu.Instance && (RichTextMenu.Instance.brushMarks = new Set()); } @action fillBrush() { if (!this.view) return; if (!Array.from(this.brushMarks.keys()).length) { const selectedMarks = this.getMarksInSelection(this.view.state); if (selectedMarks.size >= 0) { this.brushMarks = selectedMarks; } } else { const { from, to, $from } = this.view.state.selection; if (!this.view.state.selection.empty && $from && $from.nodeAfter) { if (to - from > 0) { this.view.dispatch(this.view.state.tr.removeMark(from, to)); Array.from(this.brushMarks) .filter(m => m.type !== schema.marks.user_mark) .forEach((mark: Mark) => { this.setMark(mark, this.view!.state, this.view!.dispatch); }); } } } } get TextView() { return this.view?.TextView; } get TextViewFieldKey() { return this.TextView?._props.fieldKey; } @action setActiveHighlight(color: string) { this._activeHighlightColor = color; } @action setCurrentLink(link: string) { this.currentLink = link; } createLinkButton() { const onLinkChange = (e: React.ChangeEvent) => { this.TextView?.endUndoTypingBatch(); UndoManager.RunInBatch(() => this.setCurrentLink(e.target.value), 'link change'); }; const link = this.currentLink ? this.currentLink : ''; const button = ( set hyperlink} placement="bottom"> { } ); const dropdownContent = (

Linked to:

); // eslint-disable-next-line no-use-before-define return ; } async getTextLinkTargetTitle() { if (!this.view) return undefined; const node = this.view.state.selection.$from.nodeAfter; const link = node && node.marks.find(m => m.type.name === 'link'); if (link) { const href = link.attrs.allAnchors.length > 0 ? link.attrs.allAnchors[0].href : undefined; if (href) { if (href.indexOf(Doc.localServerPath()) === 0) { const linkclicked = href.replace(Doc.localServerPath(), '').split('?')[0]; if (linkclicked) { const linkDoc = await DocServer.GetRefField(linkclicked); if (linkDoc instanceof Doc) { const linkAnchor1 = await Cast(linkDoc.link_anchor_1, Doc); const linkAnchor2 = await Cast(linkDoc.link_anchor_2, Doc); const currentDoc = DocumentView.Selected().lastElement().Document; if (currentDoc && linkAnchor1 && linkAnchor2) { if (Doc.AreProtosEqual(currentDoc, linkAnchor1)) { return StrCast(linkAnchor2.title); } if (Doc.AreProtosEqual(currentDoc, linkAnchor2)) { return StrCast(linkAnchor1.title); } } } } } else { return href; } } else { return link.attrs.title; } } return undefined; } // TODO: should check for valid URL @undoBatch makeLinkToURL = (target: string) => { this.TextView?.makeLinkAnchor(undefined, 'onRadd:rightight', target, target); }; @undoBatch deleteLink = () => { if (this.view) { const linkAnchor = this.view.state.selection.$from.nodeAfter?.marks.find(m => m.type === this.view!.state.schema.marks.linkAnchor); if (linkAnchor) { const allAnchors = (linkAnchor.attrs.allAnchors as { href: string; title: string; linkId: string; targetId: string }[]).slice(); this.TextView?.RemoveAnchorFromSelection(allAnchors); // bcz: Argh ... this will remove the link from the document even it's anchored somewhere else in the text which happens if only part of the anchor text was selected. allAnchors .filter(aref => aref?.href.indexOf(Doc.localServerPath()) === 0) .forEach(aref => { const anchorId = aref.href.replace(Doc.localServerPath(), '').split('?')[0]; anchorId && DocServer.GetRefField(anchorId).then(linkDoc => Doc.DeleteLink?.(linkDoc as Doc)); }); } } }; render() { return null; } } interface ButtonDropdownProps { view?: EditorView; button: JSX.Element; dropdownContent: JSX.Element; openDropdownOnButton?: boolean; link?: boolean; pdf?: boolean; } @observer export class ButtonDropdown extends ObservableReactComponent { @observable private showDropdown: boolean = false; private ref: HTMLDivElement | null = null; constructor(props: ButtonDropdownProps) { super(props); makeObservable(this); } componentDidMount() { document.addEventListener('pointerdown', this.onBlur); } componentWillUnmount() { document.removeEventListener('pointerdown', this.onBlur); } @action setShowDropdown(show: boolean) { this.showDropdown = show; } @action toggleDropdown() { this.showDropdown = !this.showDropdown; } onDropdownClick = (e: React.PointerEvent) => { e.preventDefault(); e.stopPropagation(); this.toggleDropdown(); }; onBlur = (e: PointerEvent) => { setTimeout(() => { if (this.ref !== null && !this.ref.contains(e.target as Node)) { this.setShowDropdown(false); } }, 0); }; render() { return (
{ this.ref = node; }}> {!this._props.pdf ? (
{this._props.button}
) : ( <> {this._props.button} { } )} {this.showDropdown ? this._props.dropdownContent : null}
); } } interface RichTextMenuPluginProps { editorProps: FormattedTextBoxProps; } export class RichTextMenuPlugin extends React.Component { update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) { RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.dataDoc); } render() { return null; } }