diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 312 |
1 files changed, 134 insertions, 178 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 3b21df2ed..dcdac6775 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,9 +1,8 @@ /* eslint-disable no-use-before-define */ -/* eslint-disable jsx-a11y/no-static-element-interactions */ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; -import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; @@ -14,7 +13,7 @@ import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transacti import { EditorView, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, StopEvent } from '../../../../ClientUtils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, returnFalse, returnZero, setupMoveUpEvents, simMouseEvent, smoothScroll, StopEvent } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; @@ -77,6 +76,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } + public static MakeConfig(rules?: RichTextRules, props?: FormattedTextBoxProps) { + const keymapping = buildKeymap(schema, props ?? {}); + return { + schema, + plugins: [ + inputRules(rules?.inpRules ?? { rules: [] }), + ...(props ? [FormattedTextBox.richTextMenuPlugin(props)] : []), + history(), + keymap(keymapping), + keymap(baseKeymap), + new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), + new Plugin({ view: () => new FormattedTextBoxComment() }), + ], + }; + } private static nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; /** * Initialize the class with all the plugin node view components @@ -92,12 +106,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB static _bulletStyleSheet = addStyleSheet(); static _userStyleSheet = addStyleSheet(); static _hadSelection: boolean = false; + private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef<SidebarAnnos>(); private _sidebarTagRef = React.createRef<React.Component>(); private _ref: React.RefObject<HTMLDivElement> = React.createRef(); private _scrollRef: HTMLDivElement | null = null; - private _editorView: Opt<EditorView>; + private _editorView: Opt<EditorView & { TextView?: FormattedTextBox | undefined }>; public _applyingChange: string = ''; private _inDrop = false; private _finishingLink = false; @@ -109,79 +124,35 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _recordingStart: number = 0; private _ignoreScroll = false; private _focusSpeed: Opt<number>; - private _keymap: any = undefined; private _rules: RichTextRules | undefined; private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason. This flags the 2nd click as happening on a selection which allows bullet points to toggle private _break = true; public ProseRef?: HTMLDivElement; - public get EditorView() { - return this._editorView; - } - public get SidebarKey() { - return this.fieldKey + '_sidebar'; - } - @computed get allSidebarDocs() { - return DocListCast(this.dataDoc[this.SidebarKey]); - } - - @computed get noSidebar() { - return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; - } - @computed get layout_sidebarWidthPercent() { - return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); - } - @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); - } - @computed get layout_autoHeight() { - return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; - } - @computed get textHeight() { - return NumCast(this.dataDoc[this.fieldKey + '_height']); - } - @computed get scrollHeight() { - return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); - } - @computed get sidebarHeight() { - return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); - } - @computed get titleHeight() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0; - } - @computed get layout_autoHeightMargins() { - return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); - } - @computed get _recordingDictation() { - return this.dataDoc?.mediaState === mediaState.Recording; - } set _recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } + @computed get _recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore + @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } // prettier-ignore + @computed get noSidebar() { return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar; } // prettier-ignore + @computed get layout_sidebarWidthPercent() { return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } // prettier-ignore + @computed get sidebarColor() { return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } // prettier-ignore + @computed get layout_autoHeight() { return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight; } // prettier-ignore + @computed get textHeight() { return NumCast(this.dataDoc[this.fieldKey + '_height']); } // prettier-ignore + @computed get scrollHeight() { return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']); } // prettier-ignore + @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']); } // prettier-ignore + @computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore + @computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore @computed get config() { - this._keymap = buildKeymap(schema, this._props); this._rules = new RichTextRules(this.Document, this); - return { - schema, - plugins: [ - inputRules(this._rules.inpRules), - this.richTextMenuPlugin(), - history(), - keymap(this._keymap), - keymap(baseKeymap), - new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), - new Plugin({ - view(/* editorView */) { - return new FormattedTextBoxComment(); - }, - }), - ], - }; + return FormattedTextBox.MakeConfig(this._rules, this._props); } - // State for GPT - @observable - private gptRes: string = ''; - + public get EditorView() { + return this._editorView; + } + public get SidebarKey() { + return this.fieldKey + '_sidebar'; + } public makeAIFlashcards: () => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; @@ -206,9 +177,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (state && a1 && a2 && this._editorView) { this.removeDocument(a1); this.removeDocument(a2); - let allFoundLinkAnchors: any[] = []; - state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any /* , pos: number, parent: any */) => { - const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; + let allFoundLinkAnchors: { href: string; title: string; anchorId: string }[] = []; + state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: Node /* , pos: number, parent: any */) => { + const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: { href: string; title: string; anchorId: string }) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || []; allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; }); @@ -256,7 +227,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; - let stopFunc: any; + let stopFunc: () => void = emptyFunction; const targetData = target[DocData]; targetData.mediaState = mediaState.Recording; DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore @@ -274,10 +245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); }; - AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'); - return undefined; - }, 'highlght text'); + AnchorMenu.Instance.Highlight = undoable((color: string) => this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'), 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); AnchorMenu.Instance.StartCropDrag = unimplementedFunction; /** @@ -293,7 +261,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return target; }; - DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); + const docView = this.DocumentView?.(); + docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); @@ -307,6 +276,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ele.append(contents); } this._selectionHTML = ele?.innerHTML; + // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /* empty */ } @@ -346,7 +316,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; - state.tr.doc.nodesBetween(0, state.doc.content.size, (node: any /* , pos: number, parent: any */) => { + state.tr.doc.nodesBetween(0, state.doc.content.size, (node: Node /* , pos: number, parent: any */) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { accumTags.push(node.attrs.fieldKey); } @@ -411,8 +381,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); if (this._editorView && linkTime) { const { state } = this._editorView; - const { path } = state.selection.$from as any; - if (linkAnchor && path[path.length - 3].type !== state.schema.nodes.code_block) { + const node = state.selection.$from.node(); + if (linkAnchor && node.type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; const { from } = state.selection; @@ -477,7 +447,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB * function of a freeform view that is driven by the text box's text. The include directive will copy the code of the published * document into the code being evaluated. */ - hyperlinkTerm = (trIn: any, target: Doc, newAutoLinks: Set<Doc>) => { + hyperlinkTerm = (trIn: Transaction, target: Doc, newAutoLinks: Set<Doc>) => { let tr = trIn; const editorView = this._editorView; if (editorView && !Doc.AreProtosEqual(target, this.Document)) { @@ -494,7 +464,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ) { const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); tr = tr.addMark(sel.from, sel.to, splitter); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number /* , parent: any */) => { + tr.doc.nodesBetween(sel.from, sel.to, (node: Node, pos: number /* , parent: any */) => { if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? @@ -593,7 +563,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const draggedDoc = dragData.droppedDocuments.lastElement(); let added: Opt<boolean>; const dropAction = dragData.dropAction || dragData.userDropAction; - if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) { + if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) { // replace text contents when dragging with Alt if (de.altKey) { const fieldKey = Doc.LayoutFieldKey(draggedDoc); @@ -647,15 +617,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (node.isBlock) { // tslint:disable-next-line: prefer-for-of - for (let i = 0; i < (context.content as any).content.length; i++) { - const result = this.getNodeEndpoints((context.content as any).content[i], node); + for (let i = 0; i < context.content.childCount; i++) { + const result = this.getNodeEndpoints(context.content.child(i), node); if (result) { return { from: result.from + offset + (context.type.name === 'doc' ? 0 : 1), to: result.to + offset + (context.type.name === 'doc' ? 0 : 1), }; } - offset += (context.content as any).content[i].nodeSize; + offset += context.content.child(i).nodeSize; } } return null; @@ -819,10 +789,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> - while (target && !target.dataset?.targethrefs) target = target.parentElement; + let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span> + while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement; const editor = this._editorView; - if (editor && target && !(e.nativeEvent as any).dash) { + if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -831,10 +801,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB .lastElement() .replace(Doc.localServerPath(), '') .split('?')[0]; - const deleteMarkups = undoBatch(() => { + const deleteMarkups = undoable(() => { const { selection } = editor.state; editor.dispatch(editor.state.tr.removeMark(selection.from, selection.to, editor.state.schema.marks.linkAnchor)); - }); + }, 'delete markups'); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( @@ -858,21 +828,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const changeItems: ContextMenuProps[] = []; changeItems.push({ description: 'plain', - event: undoBatch(() => { + event: undoable(() => { Doc.setNativeView(this.Document); this.layoutDoc.layout_autoHeightMargins = undefined; - }), + }, 'set plain view'), icon: 'eye', }); changeItems.push({ description: 'metadata', - event: undoBatch(() => { + event: undoable(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; this.Document.layout_fieldKey = 'layout_meta'; setTimeout(() => { this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; }, 50); - }), + }, 'set metadata view'), icon: 'eye', }); const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null); @@ -880,11 +850,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const icon: IconProp = StrCast(note.icon) as IconProp; changeItems.push({ description: StrCast(note.title), - event: undoBatch(() => { - this.layoutDoc.layout_autoHeightMargins = undefined; - Doc.setNativeView(this.Document); - DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); - }), + event: undoable( + () => { + this.layoutDoc.layout_autoHeightMargins = undefined; + Doc.setNativeView(this.Document); + DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note); + }, + `set ${StrCast(note.title)} view}` + ), icon: icon, }); }); @@ -906,7 +879,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }) ); const appearance = cm.findByDescription('Appearance...'); - const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; + const appearanceItems = appearance?.subitems ?? []; appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', @@ -961,7 +934,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); - const optionItems = options && 'subitems' in options ? options.subitems : []; + const optionItems = options?.subitems ?? []; optionItems.push({ description: `Toggle auto update from template`, event: () => { @@ -990,7 +963,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const help = cm.findByDescription('Help...'); - const helpItems = help && 'subitems' in help ? help.subitems : []; + const helpItems = help?.subitems ?? []; helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: <BsMarkdownFill /> }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; @@ -1108,7 +1081,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); - tr.doc.nodesBetween(selection.from, selection.to, (node: any, pos: number /* , parent: any */) => { + tr.doc.nodesBetween(selection.from, selection.to, (node: Node, pos: number /* , parent: any */) => { if (node.firstChild === null && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { const allAnchors = [{ href, title, anchorId: anchor[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.linkAnchor.name)?.attrs.allAnchors ?? [])); @@ -1185,17 +1158,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._didScroll = false; // assume we don't need to scroll. if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below if (this._editorView && textAnchorId) { - const editor = this._editorView; - const ret = findAnchorFrag(editor.state.doc.content, editor); + const { state } = this._editorView; + const ret = findAnchorFrag(state.doc.content, this._editorView); - const content = (ret.frag as any)?.content; - if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) { + const firstChild = ret.frag.childCount ? ret.frag.child(0) : undefined; + if (ret.start >= 0 && (ret.frag.size || (firstChild && [state.schema.nodes.dashDoc, state.schema.nodes.audioTag].includes(firstChild.type)))) { !options.instant && (this._focusSpeed = focusSpeed); - let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start + let selection = TextSelection.near(state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { - selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected + selection = TextSelection.between(state.doc.resolve(ret.start), state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected } - editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); + this._editorView.dispatch(state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView()); const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId; addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' }); setTimeout(() => { @@ -1271,9 +1244,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const protoData = DocCast(this.dataDoc.proto)?.[this.fieldKey]; const dataData = this.dataDoc[this.fieldKey]; const layoutData = Doc.AreProtosEqual(this.layoutDoc, this.dataDoc) ? undefined : this.layoutDoc[this.fieldKey]; - const dataTime = dataData ? DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; - const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; - const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0 : 0; + const dataTime = dataData ? (DateCast(this.dataDoc[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const layoutTime = layoutData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.layoutDoc)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; + const protoTime = protoData && this.dataDoc[this.fieldKey + '_autoUpdate'] ? (DateCast(DocCast(this.dataDoc.proto)[this.fieldKey + '_modificationDate'])?.date.getTime() ?? 0) : 0; const recentData = dataTime >= layoutTime ? (protoTime >= dataTime ? protoData : dataData) : layoutTime >= protoTime ? layoutData : protoData; const whichData = recentData ?? (this.layoutDoc.isTemplateDoc ? layoutData : protoData) ?? protoData; return !whichData ? undefined : { data: RTFCast(whichData), str: Field.toString(DocCast(whichData) ?? StrCast(whichData)) }; @@ -1406,41 +1379,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let el = elIn; while (el && el !== document.body) { if (getComputedStyle(el).display === 'none') return false; - el = el.parentNode as any; + el = el.parentElement; } return true; } - richTextMenuPlugin() { - const self = this; + static richTextMenuPlugin(props: FormattedTextBoxProps) { return new Plugin({ - view(newView) { - runInAction(() => { - self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); - }); - return new RichTextMenuPlugin({ editorProps: this._props }); - }, + view: action((newView: EditorView) => { + props?.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + return new RichTextMenuPlugin({ editorProps: props }); + }), }); } _didScroll = false; _scrollStopper: undefined | (() => void); + // eslint-disable-next-line @typescript-eslint/no-explicit-any setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField); if (this.ProseRef) { - const self = this; this._editorView?.destroy(); this._editorView = new EditorView(this.ProseRef, { state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config), handleScrollToSelection: editorView => { const docPos = editorView.coordsAtPos(editorView.state.selection.to); - const viewRect = self._ref.current!.getBoundingClientRect(); - const scrollRef = self._scrollRef; + const viewRect = this._ref.current!.getBoundingClientRect(); + const scrollRef = this._scrollRef; const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined; const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined; if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) { const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); - const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; + const scrollPos = scrollRef.scrollTop + shift * this.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { setTimeout(() => { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); @@ -1471,7 +1441,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }); } } - (this._editorView as any).TextView = this; + this._editorView.TextView = this; } const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); @@ -1496,7 +1466,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } else if (!FormattedTextBox.DontSelectInitialText) { const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); selectAll(this._editorView.state, (tx: Transaction) => { - this._editorView?.dispatch(tx.deleteSelection().addStoredMark(mark)); + this._editorView?.dispatch(tx.addStoredMark(mark)); }); this.tryUpdateDoc(true); // calling select() above will make isContentActive() true only after a render .. which means the selectAll() above won't write to the Document and the incomingValue will overwrite the selection with the non-updated data } else { @@ -1550,18 +1520,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } onPointerDown = (e: React.PointerEvent): void => { - if ((e.nativeEvent as any).handledByInnerReactInstance) { - return; // e.stopPropagation(); - } - (e.nativeEvent as any).handledByInnerReactInstance = true; - if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. - if ((e.target as any).tagName === 'AUDIOTAG') { + const target = e.target as HTMLElement; + if (target.tagName === 'AUDIOTAG') { e.preventDefault(); e.stopPropagation(); - const timecode = Number((e.target as any)?.dataset?.timecode); - DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => { + const timecode = Number(target.dataset?.timecode); + DocServer.GetRefField(target.dataset?.audioid || '').then(anchor => { if (anchor instanceof Doc) { // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; @@ -1585,7 +1551,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes. e.stopPropagation(); // if the text box's content is active, then it consumes all down events document.addEventListener('pointerup', this.onSelectEnd); - (this.ProseRef?.children?.[0] as any).focus(); + (this.ProseRef?.children?.[0] as HTMLElement).focus(); } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { @@ -1601,10 +1567,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const state = this.EditorView?.state; if (state && this.ProseRef?.children[0].className.includes('-focused') && this._props.isContentActive() && !e.button) { if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); - let clickTarget = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - for (let { target } = e as any; target && !target.dataset?.targethrefs; target = target.parentElement); - while (clickTarget && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, clickTarget?.dataset?.targethrefs, clickTarget?.dataset.linkdoc, clickTarget?.dataset.nopreview === 'true'); + let clickTarget: HTMLElement | Element | null = e.target as HTMLElement; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> + for (let target: HTMLElement | Element | null = clickTarget as HTMLElement; target instanceof HTMLElement && !target.dataset?.targethrefs; target = target.parentElement); + while (clickTarget instanceof HTMLElement && !clickTarget.dataset?.targethrefs) clickTarget = clickTarget.parentElement; + const dataset = clickTarget instanceof HTMLElement ? clickTarget?.dataset : undefined; + FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true'); } }; @action @@ -1628,27 +1595,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setFocus = (ipos?: number) => { const pos = ipos ?? (this._editorView?.state.selection.$from.pos || 1); setTimeout(() => this._editorView?.dispatch(this._editorView.state.tr.setSelection(TextSelection.near(this._editorView.state.doc.resolve(pos)))), 100); - setTimeout(() => (this.ProseRef?.children?.[0] as any).focus(), 200); + setTimeout(() => (this.ProseRef?.children?.[0] as HTMLElement).focus(), 200); }; @action onFocused = (e: React.FocusEvent): void => { // applyDevTools.applyDevTools(this._editorView); - this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { if (!this._props.isContentActive()) return; - if ((e.nativeEvent as any).handledByInnerReactInstance) { - e.stopPropagation(); - return; - } - if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) { + const editorView = this._editorView; + const editorRoot = editorView?.root instanceof Document ? editorView.root : undefined; + if (editorView && (!this._forceUncollapse || editorRoot?.getSelection()?.isCollapsed)) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text. - const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) - if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2))); + const pcords = editorView.posAtCoords({ left: e.clientX, top: e.clientY }); + const node = pcords && editorView.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text) + if (pcords && node?.type === editorView.state.schema.nodes.dashComment) { + this._editorView!.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, pcords.pos + 2))); e.preventDefault(); } if (!node && this.ProseRef) { @@ -1656,19 +1620,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const boundsRect = lastNode?.getBoundingClientRect(); if (e.clientX > boundsRect.left && e.clientX < boundsRect.right && e.clientY > boundsRect.bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document - this._editorView?.focus(); - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); + editorView.focus(); + editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); } - } else if (node && [this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node.type) && node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos))); + } else if (node && [editorView.state.schema.nodes.ordered_list, editorView.state.schema.nodes.listItem].includes(node.type) && node !== (editorView.state.selection as NodeSelection)?.node && pcords) { + editorView.dispatch(editorView.state.tr.setSelection(NodeSelection.create(editorView.state.doc, pcords.pos))); } } - if (this._props.rootSelected?.()) { + if (editorView && this._props.rootSelected?.()) { // if text box is selected, then it consumes all click events - (e.nativeEvent as any).handledByInnerReactInstance = true; - this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, e.shiftKey); + e.stopPropagation(); + this.hitBulletTargets(e.clientX, e.clientY, !editorView.state.selection.empty || this._forceUncollapse, false, e.shiftKey); } - this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; + this._forceUncollapse = !editorRoot?.getSelection()?.isCollapsed; }; // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them. hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) { @@ -1684,9 +1648,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let $olistPos = this._editorView?.state.doc.resolve(olistPos); let olistNode = (nodeBef !== null || clickNode?.type === this._editorView?.state.schema.nodes.list_item) && olistPos === clickPos?.pos ? clickNode : nodeBef; if (olistNode?.type === this._editorView?.state.schema.nodes.list_item) { - if ($olistPos && ($olistPos as any).path.length > 3) { + if ($olistPos && $olistPos.depth) { olistNode = $olistPos.parent; - $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]); + $olistPos = this._editorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1)); } } const maxSize = this._editorView?.state.doc.content.size ?? 0; @@ -1717,7 +1681,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @action - onBlur = (e: any) => { + onBlur = (e: React.FocusEvent) => { if (this.ProseRef?.children[0] !== e.nativeEvent.target) return; if (!(this.EditorView?.state.selection instanceof NodeSelection) || this.EditorView.state.selection.node.type !== this.EditorView.state.schema.nodes.footnote) { const stordMarks = this._editorView?.state.storedMarks?.slice(); @@ -1782,7 +1746,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB switch (e.key) { case 'Escape': this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); - (document.activeElement as any).blur?.(); + (document.activeElement as HTMLElement).blur?.(); DocumentView.DeselectAll(); RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; @@ -1888,7 +1852,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB TraceMobx(); const annotated = DocListCast(this.dataDoc[this.SidebarKey]).filter(d => d?.author).length; const color = !annotated ? Colors.WHITE : Colors.BLACK; - const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')); + const backgroundColor = !annotated ? (this.sidebarWidth() ? Colors.MEDIUM_BLUE : Colors.BLACK) : (this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.WidgetColor + (annotated ? ':annotated' : '')) as string); return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : ( <div @@ -1905,6 +1869,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get sidebarCollection() { const renderComponent = (tag: string) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const ComponentTag: any = tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos @@ -2031,19 +1996,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); } }; - _oldWheel: any; - @computed get fontColor() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor); - } - @computed get fontSize() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); - } - @computed get fontFamily() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily); - } - @computed get fontWeight() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight); - } + @computed get fontColor() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore + @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore + @computed get fontFamily() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore + @computed get fontWeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore render() { TraceMobx(); const scale = this._props.NativeDimScaling?.() || 1; @@ -2082,7 +2038,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB style={{ cursor: this._props.isContentActive() ? 'text' : undefined, height: this._props.height ? 'max-content' : undefined, - overflow: this.layout_autoHeight ? 'hidden' : undefined, pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none', }} onContextMenu={this.specificContextMenu} @@ -2141,6 +2096,7 @@ Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { _layout_nativeDimEditable: true, _layout_reflowVertical: true, _layout_reflowHorizontal: true, + _layout_noSidebar: true, defaultDoubleClick: 'ignore', systemIcon: 'BsFileEarmarkTextFill', }, |