diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 651 |
1 files changed, 331 insertions, 320 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 43010b2ed..e62a639c8 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,3 +1,5 @@ +/* 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'; @@ -9,11 +11,12 @@ import { inputRules } from 'prosemirror-inputrules'; import { keymap } from 'prosemirror-keymap'; import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, Selection, TextSelection, Transaction } from 'prosemirror-state'; -import { EditorView } from 'prosemirror-view'; +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 { DateField } from '../../../../fields/DateField'; -import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc'; +import { CreateLinkToActiveAudio, Doc, DocListCast, Field, FieldType, Opt, StrListCast } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; @@ -23,42 +26,38 @@ import { RichTextField } from '../../../../fields/RichTextField'; import { ComputedField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; +import { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; -import { Docs, DocUtils } from '../../../documents/Documents'; -import { CollectionViewType } from '../../../documents/DocumentTypes'; +import { Docs } from '../../../documents/Documents'; +import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; +import { DocUtils } from '../../../documents/DocUtils'; import { DictationManager } from '../../../util/DictationManager'; -import { DocumentManager } from '../../../util/DocumentManager'; -import { DragManager, dropActionType } from '../../../util/DragManager'; +import { DragManager } from '../../../util/DragManager'; +import { dropActionType } from '../../../util/DropActionTypes'; import { MakeTemplate } from '../../../util/DropConverter'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; -import { SelectionManager } from '../../../util/SelectionManager'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; -import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; -import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; -import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; +import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; -import { StyleProp } from '../../StyleProvider'; -import { media_state } from '../AudioBox'; -import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView'; -import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView'; +import { StyleProp } from '../../StyleProp'; +import { styleFromLayoutString } from '../../StyleProvider'; +import { mediaState } from '../AudioBox'; +import { DocumentView } from '../DocumentView'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { FocusViewOptions } from '../FocusViewOptions'; import { LinkInfo } from '../LinkDocPreview'; -import { PinProps, PresBox } from '../trails'; -import { DashDocCommentView } from './DashDocCommentView'; -import { DashDocView } from './DashDocView'; -import { DashFieldView } from './DashFieldView'; -import { EquationView } from './EquationView'; -import { FootnoteView } from './FootnoteView'; +import { OpenWhere } from '../OpenWhere'; import './FormattedTextBox.scss'; import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer'; @@ -66,21 +65,26 @@ import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; -import { SummaryView } from './SummaryView'; // import * as applyDevTools from 'prosemirror-dev-tools'; -interface FormattedTextBoxProps extends FieldViewProps { +export interface FormattedTextBoxProps extends FieldViewProps { onBlur?: () => void; // callback when text loses focus autoFocus?: boolean; // whether text should get input focus when created } @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } - public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); - public static Instance: FormattedTextBox; - public static LiveTextUndo: UndoManager.Batch | undefined; + private static nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; + /** + * Initialize the class with all the plugin node view components + * @param nodeViews prosemirror plugins that render a custom UI for specific node types + */ + public static Init(nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }) { + FormattedTextBox.nodeViews = nodeViews; + } + public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch when typing a new text note into a collection static _globalHighlightsCache: string = ''; static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']); static _highlightStyleSheet: any = addStyleSheet(); @@ -97,7 +101,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; - private _lastTimedMark: Mark | undefined = undefined; private _cachedLinks: Doc[] = []; private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -108,10 +111,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB 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 _forceDownNode: Node | undefined; - private _downX = 0; - private _downY = 0; - private _downTime = 0; private _break = true; public ProseRef?: HTMLDivElement; public get EditorView() { @@ -152,10 +151,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } @computed get _recordingDictation() { - return this.dataDoc?.mediaState === media_state.Recording; + return this.dataDoc?.mediaState === mediaState.Recording; } set _recordingDictation(value) { - !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined); + !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } @computed get config() { this._keymap = buildKeymap(schema, this._props); @@ -170,8 +169,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB keymap(baseKeymap), new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), new Plugin({ - view(editorView) { - return new FormattedTextBoxComment(editorView); + view(/* editorView */) { + return new FormattedTextBoxComment(); }, }), ], @@ -183,31 +182,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB private gptRes: string = ''; public static PasteOnLoad: ClipboardEvent | undefined; - private static SelectOnLoad: Doc | undefined; - public static SetSelectOnLoad(doc: Doc) { - FormattedTextBox.SelectOnLoad = doc; - } public static DontSelectInitialText = false; // whether initial text should be selected or not public static SelectOnLoadChar = ''; - public static IsFragment(html: string) { - return html.indexOf('data-pm-slice') !== -1; - } - public static GetHref(html: string): string { - const parser = new DOMParser(); - const parsedHtml = parser.parseFromString(html, 'text/html'); - if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) { - return (parsedHtml.body.childNodes[0].childNodes[0] as any).href; - } - return ''; - } - public static GetDocFromUrl(url: string) { - return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId - } constructor(props: FormattedTextBoxProps) { super(props); makeObservable(this); - FormattedTextBox.Instance = this; this._recordingStart = Date.now(); } @@ -217,13 +197,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB public RemoveLinkFromDoc(linkDoc?: Doc) { this.unhighlightSearchTerms(); const state = this._editorView?.state; - const a1 = linkDoc?.link_anchor_1 as Doc; - const a2 = linkDoc?.link_anchor_2 as Doc; + const a1 = DocCast(linkDoc?.link_anchor_1); + const a2 = DocCast(linkDoc?.link_anchor_2); if (state && a1 && a2 && this._editorView) { this.removeDocument(a1); this.removeDocument(a2); - var allFoundLinkAnchors: any[] = []; - state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => { + 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]) || []; allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors; return true; @@ -246,26 +226,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); + const rootDoc: Doc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document); if (!pinProps && this._editorView?.state.selection.empty) return rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); this._finishingLink = false; - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); + PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); return anchor; }; @action setupAnchorMenu = () => { AnchorMenu.Instance.Status = 'marquee'; - - AnchorMenu.Instance.OnClick = (e: PointerEvent) => { + AnchorMenu.Instance.OnClick = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created }; - AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { + AnchorMenu.Instance.OnAudio = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); @@ -275,8 +254,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB anchor.followLinkAudio = true; let stopFunc: any; const targetData = target[DocData]; - targetData.mediaState = media_state.Recording; - DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop)); + targetData.mediaState = mediaState.Recording; + DictationManager.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => { stopFunc = stop }); // prettier-ignore + const reactionDisposer = reaction( () => target.mediaState, dictation => { @@ -286,12 +266,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } ); - target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); + target.title = ComputedField.MakeFunction(`this.text_audioAnnotations_text.lastElement()`); } }); }; AnchorMenu.Instance.Highlight = undoable((color: string) => { - this._editorView?.state && RichTextMenu.Instance?.setHighlight(color); + this._editorView?.state && RichTextMenu.Instance?.setFontField(color, 'fontHighlight'); return undefined; }, 'highlght text'); AnchorMenu.Instance.onMakeAnchor = () => this.getAnchor(true); @@ -305,15 +285,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn); - FormattedTextBox.SetSelectOnLoad(target); + Doc.SetSelectOnLoad(target); return target; }; DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); + + AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); - let ele: Opt<HTMLDivElement> = undefined; + let ele: Opt<HTMLDivElement>; try { const contents = window.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { @@ -321,7 +303,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ele.append(contents); } this._selectionHTML = ele?.innerHTML; - } catch (e) {} + } catch (e) { + /* empty */ + } }; leafText = (node: Node) => { @@ -330,13 +314,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const fieldKey = StrCast(node.attrs.fieldKey); return ( (node.attrs.hideKey ? '' : fieldKey + ':') + // - (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as Field)) + (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as FieldType)) ); } return ''; }; dispatchTransaction = (tx: Transaction) => { - if (this._editorView && (this._editorView as any).docView) { + if (this._editorView && !this._editorView.isDestroyed) { const state = this._editorView.state.apply(tx); this._editorView.updateState(state); this.tryUpdateDoc(false); @@ -344,13 +328,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; tryUpdateDoc = (force: boolean) => { - if (this._editorView && (this._editorView as any).docView) { - const state = this._editorView.state; - const dataDoc = this.dataDoc; + if (this._editorView) { + const { state } = this._editorView; + const { dataDoc } = this; const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText); const newJson = JSON.stringify(state.toJSON()); const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box - const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype const effectiveAcl = GetEffectiveAcl(dataDoc); @@ -359,7 +342,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: any /* , pos: number, parent: any */) => { if (node.type === schema.nodes.dashField && node.attrs.fieldKey.startsWith('#')) { accumTags.push(node.attrs.fieldKey); } @@ -370,6 +353,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB let unchanged = true; const textChange = newText !== prevData?.Text; // the Text string can change even if the RichText doesn't because dashFieldViews may return new strings as the data they reference changes + const rtField = (layoutData !== prevData ? layoutData : undefined) ?? protoData; if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) { this._applyingChange = this.fieldKey; textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); @@ -383,10 +367,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._applyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false unchanged = false; } - } else { + } else if (rtField) { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data))); + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(rtField.Data))); ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } @@ -414,37 +398,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB insertTime = () => { let linkTime; let linkAnchor; - let link; - LinkManager.Links(this.dataDoc).forEach((l, i) => { - const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; - if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) { + Doc.Links(this.dataDoc).forEach(l => { + const anchor = DocCast(l.link_anchor_1)?.annotationOn ? DocCast(l.link_anchor_1) : DocCast(l.link_anchor_2)?.annotationOn ? DocCast(l.link_anchor_2) : undefined; + if (anchor && (anchor.annotationOn as Doc).mediaState === mediaState.Recording) { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; - link = l; } }); if (this._editorView && linkTime) { - const state = this._editorView.state; - const now = Date.now(); - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) }); - if (!this._break && state.selection.to !== state.selection.from) { - for (let i = state.selection.from; i <= state.selection.to; i++) { - const pos = state.doc.resolve(i); - const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark); - if (um) { - mark = um; - break; - } - } - } - - const path = (this._editorView.state.selection.$from as any).path; - if (linkAnchor && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) { + const { state } = this._editorView; + const { path } = state.selection.$from as any; + if (linkAnchor && path[path.length - 3].type !== state.schema.nodes.code_block) { const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000; this._break = false; - const from = state.selection.from; - const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); - const replaced = this._editorView.state.tr.insert(from - 1, value); + const { from } = state.selection; + const value = state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] }); + const replaced = state.tr.insert(from - 1, value); this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1)))); } } @@ -452,7 +421,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.Document).filter( + const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.isTemplateForField(this.Document) && (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && @@ -461,17 +430,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this._editorView?.state.doc.textContent) { - const f = this._editorView.state.selection.from; - - const t = this._editorView.state.selection.to; - var tr = this._editorView.state.tr as any; - const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor; - tr = tr.removeMark(0, tr.doc.content.size, autoAnch); - Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks))); - tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); + let { tr } = this._editorView.state; + const { from, to } = this._editorView.state.selection; + const { autoLinkAnchor } = this._editorView.state.schema.marks; + tr = tr.removeMark(0, tr.doc.content.size, autoLinkAnchor); + Doc.MyPublishedDocs.filter(term => term.title).forEach(term => { + tr = this.hyperlinkTerm(tr, term, newAutoLinks); + }); + tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))); this._editorView?.dispatch(tr); } - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink); + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); }; updateTitle = () => { @@ -504,11 +473,12 @@ 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 = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { + hyperlinkTerm = (trIn: any, target: Doc, newAutoLinks: Set<Doc>) => { + let tr = trIn; const editorView = this._editorView; - if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) { - const autoLinkTerm = Field.toString(target.title as Field).replace(/^@/, ''); - var alink: Doc | undefined; + if (editorView && !Doc.AreProtosEqual(target, this.Document)) { + const autoLinkTerm = Field.toString(target.title as FieldType).replace(/^@/, ''); + let alink: Doc | undefined; this.findInNode(editorView, editorView.state.doc, autoLinkTerm).forEach(sel => { if ( !sel.$anchor.pos || @@ -520,11 +490,11 @@ 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: any, 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 ?? - (LinkManager.Links(this.Document).find( + (Doc.Links(this.Document).find( link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.Document) && // Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target) @@ -551,12 +521,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; }; highlightSearchTerms = (terms: string[], backward: boolean) => { - if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); - const length = res[0].length; - let tr = this._editorView.state.tr; + const { _editorView } = this; + if (_editorView && terms.some(t => t)) { + const { state } = _editorView; + let { tr } = state; + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const res = terms.filter(t => t).map(term => this.findInNode(_editorView, state.doc, term)); + const { length } = res[0]; const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; @@ -571,23 +543,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } const lastSel = Math.min(flattened.length - 1, this._searchIndex); - flattened.forEach((h: TextSelection, ind: number) => (tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark))); - flattened[lastSel] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); + flattened.forEach((h: TextSelection, ind: number) => { + tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark); + }); + flattened[lastSel] && _editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); } }; unhighlightSearchTerms = () => { - if (window.screen.width < 600) null; - else if (this._editorView && (this._editorView as any).docView) { - const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight); - const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true }); - const end = this._editorView.state.doc.nodeSize - 2; - this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + if (this._editorView) { + const { state } = this._editorView; + if (state) { + const mark = state.schema.mark(state.schema.marks.search_highlight); + const activeMark = state.schema.mark(state.schema.marks.search_highlight, { selected: true }); + const end = state.doc.nodeSize - 2; + this._editorView.dispatch(state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); + } } }; adoptAnnotation = (start: number, end: number, mark: Mark) => { const view = this._editorView!; - const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail }); + const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; protected createDropTarget = (ele: HTMLDivElement) => { @@ -631,7 +607,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB float: 'unset', }); if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { - added = dragData.removeDocument?.(draggedDoc) ? true : false; + added = !!dragData.removeDocument?.(draggedDoc); } else { added = true; } @@ -643,9 +619,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._inDrop = true; const pos = view.posAtCoords({ left: de.x, top: de.y })?.pos; pos && view.dispatch(view.state.tr.insert(pos, node)); - added = pos ? true : false; // pos will be null if you don't drop onto an actual text location - } catch (e) { - console.log('Drop failed', e); + added = !!pos; // pos will be null if you don't drop onto an actual text location + } catch (err) { + console.log('Drop failed', err); added = false; } finally { this._inDrop = false; @@ -677,29 +653,28 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } offset += (context.content as any).content[i].nodeSize; } - return null; - } else { - return null; } + return null; } - //Recursively finds matches within a given node + // Recursively finds matches within a given node findInNode(pm: EditorView, node: Node, find: string) { let ret: TextSelection[] = []; if (node.isTextblock) { - let index = 0, - foundAt; + let index = 0; + let foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); const regexp = new RegExp(find, 'i'); if (regexp) { - var blockOffset = 0; - for (var i = 0; i < node.childCount; i++) { - var textContent = ''; + let blockOffset = 0; + for (let i = 0; i < node.childCount; i++) { + let textContent = ''; while (i < node.childCount && node.child(i).type === pm.state.schema.nodes.text) { textContent += node.child(i).textContent; i++; } + // eslint-disable-next-line no-cond-assign while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) { const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1)); ret.push(sel); @@ -710,14 +685,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } } else { - node.content.forEach((child, i) => (ret = ret.concat(this.findInNode(pm, child, find)))); + node.content.forEach(child => { + ret = ret.concat(this.findInNode(pm, child, find)); + }); } return ret; } updateHighlights = (highlights: string[]) => { if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; - setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''))); + setTimeout(() => { + FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''); + }); clearStyleSheetRules(FormattedTextBox._userStyleSheet); if (!highlights.includes('Audio Tags')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); @@ -726,7 +705,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' }); } if (highlights.includes('My Text')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' }); } if (highlights.includes('Todo Items')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' }); @@ -745,21 +724,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' }); } if (highlights.includes('By Recent Minute')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); } if (highlights.includes('By Recent Hour')) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + ClientUtils.CurrentUserEmail().replace('.', '').replace('@', ''), { opacity: '0.1' }); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } + // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } @action @@ -781,7 +761,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this, e, this.sidebarMove, - (e, movement, isClick) => !isClick && batch.end(), + (moveEv, movement, isClick) => !isClick && batch.end(), () => { this.toggleSidebar(); batch.end(); @@ -805,7 +785,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); - LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); + Doc.DeleteLink?.(Doc.Links(anchor)[0]); // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]); // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); @@ -817,7 +797,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch - makeTargetToggle = (anchor: Doc) => (anchor.followLinkToggle = !anchor.followLinkToggle); + makeTargetToggle = (anchor: Doc) => { + anchor.followLinkToggle = !anchor.followLinkToggle; + }; @undoBatch showTargetTrail = (anchor: Doc) => { @@ -833,11 +815,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; - const editor = this._editorView!; - const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); 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; - if (target && !(e.nativeEvent as any).dash) { + const editor = this._editorView; + if (editor && target && !(e.nativeEvent as any).dash) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') @@ -847,14 +828,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB .replace(Doc.localServerPath(), '') .split('?')[0]; const deleteMarkups = undoBatch(() => { - const sel = editor.state.selection; - editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + const { selection } = editor.state; + editor.dispatch(editor.state.tr.removeMark(selection.from, selection.to, editor.state.schema.marks.linkAnchor)); }); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( action(anchor => { - anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc); + anchor && DocumentView.SelectSchemaDoc(anchor as Doc); AnchorMenu.Instance.Status = 'annotation'; AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc); AnchorMenu.Instance.Pinned = false; @@ -884,7 +865,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB event: undoBatch(() => { 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); + setTimeout(() => { + this.layoutDoc._header_height = this.layoutDoc._layout_autoHeightMargins = 50; + }, 50); }), icon: 'eye', }); @@ -923,19 +906,26 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', - event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), + event: () => { + this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar; + }, icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', }); appearanceItems.push({ description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', - event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), + event: () => { + this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI; + }, icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); !Doc.noviceMode && appearanceItems.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && appearanceItems.push({ description: 'Broadcast Message', - event: () => DocServer.GetRefField('rtfProto').then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text)), + event: () => + DocServer.GetRefField('rtfProto').then(proto => { + proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.dataDoc[this.fieldKey], RichTextField)?.Text); + }), icon: 'expand-arrows-alt', }); @@ -957,27 +947,36 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const options = cm.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; - optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' }); + optionItems.push({ + description: `Toggle auto update from template`, + event: () => { + this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']; + }, + icon: 'star', + }); optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); this._props.renderDepth && optionItems.push({ description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + event: () => { + this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR; + }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); !Doc.noviceMode && optionItems.push({ description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, - event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), + event: () => { + this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight; + }, icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const help = cm.findByDescription('Help...'); const helpItems = help && 'subitems' in help ? help.subitems : []; - helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> }); + helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: <BsMarkdownFill /> }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); - this._downX = this._downY = Number.NaN; }; animateRes = (resIndex: number, newText: string) => { @@ -990,7 +989,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB askGPT = action(async () => { try { - let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); + const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { this.animateRes(0, 'Something went wrong.'); } else if (this._editorView) { @@ -1016,10 +1015,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB breakupDictation = () => { if (this._editorView && this._recordingDictation) { - this.stopDictation(true); + this.stopDictation(/* true */); this._break = true; - const state = this._editorView.state; - const to = state.selection.to; + const { state } = this._editorView; + const { to } = state.selection; const updated = TextSelection.create(state.doc, to, to); this._editorView.dispatch(state.tr.setSelection(updated).insert(to, state.schema.nodes.paragraph.create({}))); if (this._recordingDictation) { @@ -1037,7 +1036,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); }; - stopDictation = (abort: boolean) => DictationManager.Controls.stop(!abort); + stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */); setDictationContent = (value: string) => { if (this._editorView && this._recordingStart) { @@ -1046,7 +1045,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; }; - const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); + const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { link[DocData].isDictation = true; const audioanchor = Cast(link.link_anchor_2, Doc, null); @@ -1065,7 +1064,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } } } - const from = this._editorView.state.selection.from; + const { from } = this._editorView.state.selection; this._break = false; const tr = this._editorView.state.tr.insertText(value); this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, from, tr.doc.content.size)).scrollIntoView()); @@ -1074,23 +1073,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // TODO: nda -- Look at how link anchors are added makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean, addAsAnnotation?: boolean) { - const state = this._editorView?.state; - if (state) { + const { _editorView } = this; + if (_editorView) { + const { state } = _editorView; let selectedText = ''; - const sel = state.selection; + const { selection } = state; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); - let tr = state.tr.addMark(sel.from, sel.to, splitter); - if (sel.from !== sel.to) { + let tr = state.tr.addMark(selection.from, selection.to, splitter); + if (selection.from !== selection.to) { const anchor = anchorDoc ?? Docs.Create.ConfigDocument({ // - title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')', + title: 'text(' + state.doc.textBetween(selection.from, selection.to) + ')', annotationOn: this.dataDoc, }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); - tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { + tr.doc.nodesBetween(selection.from, selection.to, (node: any, 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 ?? [])); @@ -1100,7 +1100,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } }); this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents - this._editorView!.dispatch(tr.removeMark(sel.from, sel.to, splitter)); + this._editorView!.dispatch(tr.removeMark(selection.from, selection.to, splitter)); this.dataDoc[UpdatingFromServer] = this.dataDoc[ForceServerWrite] = false; anchor.text = selectedText; anchor.text_html = this._selectionHTML ?? selectedText; @@ -1121,15 +1121,19 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } - return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); + return new Promise<Opt<DocumentView>>(res => { + DocumentView.addViewRenderedCb(doc, dv => res(dv)); + }); }; focus = (textAnchor: Doc, options: FocusViewOptions) => { const focusSpeed = options.zoomTime ?? 500; const textAnchorId = textAnchor[Id]; + let start = 0; const findAnchorFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; let hadStart = start !== 0; frag.forEach((node, index) => { + // eslint-disable-next-line no-use-before-define const examinedNode = findAnchorNode(node, editor); if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) { nodes.push(examinedNode.node); @@ -1161,7 +1165,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, '')) ? { node, start: 0 } : undefined; }; - let start = 0; 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; @@ -1177,13 +1180,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB editor.dispatch(editor.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(() => (this._focusSpeed = undefined), this._focusSpeed); + setTimeout(() => { + this._focusSpeed = undefined; + }, this._focusSpeed); setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; - } else { - return this._props.focus(this.Document, options); } + return this._props.focus(this.Document, options); } + return undefined; }; // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. @@ -1199,11 +1204,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } componentDidMount() { !this._props.dontSelectOnLoad && this._props.setContentViewBox?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. - this._cachedLinks = LinkManager.Links(this.Document); + this._cachedLinks = Doc.Links(this.Document); this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation); this._disposers.layout_autoHeight = reaction( () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }), - (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) + autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction( () => Array.from(FormattedTextBox._globalHighlights).slice(), @@ -1212,21 +1217,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); this._disposers.width = reaction( () => this._props.PanelWidth(), - width => this.tryUpdateScrollHeight() + () => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( - () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight), + () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on - () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), - ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { + () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), + ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && // - layout_autoHeight && + layoutAutoHeight && newHeight && newHeight !== this.layoutDoc.height && !this._props.dontRegisterView @@ -1237,7 +1242,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') } ); this._disposers.links = reaction( - () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks + () => Doc.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks newLinks => { this._cachedLinks.forEach(l => !newLinks.includes(l) && this.RemoveLinkFromDoc(l)); this._cachedLinks = newLinks; @@ -1274,14 +1279,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.search = reaction( () => Doc.IsSearchMatch(this.Document), search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()), - { fireImmediately: Doc.IsSearchMatchUnmemoized(this.Document) ? true : false } + { fireImmediately: !!Doc.IsSearchMatchUnmemoized(this.Document) } ); this._disposers.selected = reaction( () => this._props.rootSelected?.(), action(selected => { - //selected && setTimeout(() => this.prepareForTyping()); + this.prepareForTyping(); if (FormattedTextBox._globalHighlights.has('Bold Text')) { + // eslint-disable-next-line operator-assignment this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { @@ -1299,7 +1305,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB this._disposers.record = reaction( () => this._recordingDictation, () => { - this.stopDictation(true); + this.stopDictation(/* true */); this._recordingDictation && this.recordDictation(); }, { fireImmediately: true } @@ -1322,10 +1328,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } clipboardTextSerializer = (slice: Slice): string => { - let text = '', - separated = true; - const from = 0, - to = slice.content.size; + let text = ''; + let separated = true; + const from = 0; + const to = slice.content.size; slice.content.nodesBetween( from, to, @@ -1345,9 +1351,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return text; }; - handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { + handlePaste = (view: EditorView, event: Event /* , slice: Slice */): boolean => { const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor'); - return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false; + return !!(pdfAnchorId && this.addPdfReference(pdfAnchorId)); }; addPdfReference = (pdfAnchorId: string) => { @@ -1378,7 +1384,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return false; }; - isActiveTab(el: Element | null | undefined) { + isActiveTab(elIn: Element | null | undefined) { + let el = elIn; while (el && el !== document.body) { if (getComputedStyle(el).display === 'none') return false; el = el.parentNode as any; @@ -1390,7 +1397,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const self = this; return new Plugin({ view(newView) { - runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView)); + runInAction(() => { + self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); + }); return new RichTextMenuPlugin({ editorProps: this._props }); }, }); @@ -1415,7 +1424,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE); const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { - setTimeout(() => scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper))); + setTimeout(() => { + scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); + }); } else { scrollRef.scrollTo({ top: scrollPos }); } @@ -1424,34 +1435,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB return true; }, dispatchTransaction: this.dispatchTransaction, - nodeViews: { - dashComment(node: any, view: any, getPos: any) { - return new DashDocCommentView(node, view, getPos); - }, - dashDoc(node: any, view: any, getPos: any) { - return new DashDocView(node, view, getPos, self); - }, - dashField(node: any, view: any, getPos: any) { - return new DashFieldView(node, view, getPos, self); - }, - equation(node: any, view: any, getPos: any) { - return new EquationView(node, view, getPos, self); - }, - summary(node: any, view: any, getPos: any) { - return new SummaryView(node, view, getPos); - }, - //ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); }, - footnote(node: any, view: any, getPos: any) { - return new FootnoteView(node, view, getPos); - }, - }, + nodeViews: FormattedTextBox.nodeViews(this), clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); const { state, dispatch } = this._editorView; if (!rtfField) { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc; - const startupText = Field.toString(dataDoc[fieldKey] as Field); + const startupText = Field.toString(dataDoc[fieldKey] as FieldType); if (startupText) { dispatch(state.tr.insertText(startupText)); } @@ -1465,17 +1456,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB (this._editorView as any).TextView = this; } - const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())); + const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, Doc.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); const selLoadChar = FormattedTextBox.SelectOnLoadChar; if (selectOnLoad) { - FormattedTextBox.SelectOnLoad = undefined; + Doc.SetSelectOnLoad(undefined); FormattedTextBox.SelectOnLoadChar = ''; } if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { this._props.select(false); if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; - const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks); @@ -1483,8 +1474,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const tr = tr2.setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView.state, this._editorView?.dispatch); + 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 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.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 { + const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; + const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) }); + const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; + const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; + const { tr } = this._editorView.state; + this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))).setStoredMarks(storedMarks)); 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 } } @@ -1503,17 +1506,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet. prepareForTyping = () => { - if (!this._editorView) return; - const docDefaultMarks = [ - ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), - ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), - ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []), - ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })], - ]; - this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks)); + if (this._editorView) { + const { text, paragraph } = schema.nodes; + const selNode = this._editorView.state.selection.$anchor.node(); + if (this._editorView.state.selection.from === 1 && this._editorView.state.selection.empty && [undefined, text, paragraph].includes(selNode?.type)) { + const docDefaultMarks = [schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified: Math.floor(Date.now() / 1000) })]; + this._editorView.state.selection.empty && this._editorView.state.selection.from === 1 && this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks).removeStoredMark(schema.marks.pFontColor)); + } + } }; componentWillUnmount() { @@ -1532,8 +1532,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onPointerDown = (e: React.PointerEvent): void => { if ((e.nativeEvent as any).handledByInnerReactInstance) { - return; //e.stopPropagation(); - } else (e.nativeEvent as any).handledByInnerReactInstance = true; + 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. @@ -1546,7 +1547,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB // const timecode = NumCast(anchor.timecodeToShow, 0); const audiodoc = anchor.annotationOn as Doc; const func = () => { - const docView = DocumentManager.Instance.getDocumentView(audiodoc); + const docView = DocumentView.getDocumentView(audiodoc); if (!docView) { this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); @@ -1559,9 +1560,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (this._recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } - this._downX = e.clientX; - this._downY = e.clientY; - this._downTime = Date.now(); FormattedTextBoxComment.textBox = this; if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { @@ -1575,17 +1573,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.preventDefault(); } }; - onSelectEnd = (e: PointerEvent) => { - document.removeEventListener('pointerup', this.onSelectEnd); - }; + onSelectEnd = (): void => document.removeEventListener('pointerup', this.onSelectEnd); onPointerUp = (e: React.PointerEvent): void => { 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 target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - for (let target = e.target as any; target && !target.dataset?.targethrefs; target = target.parentElement); - while (target && !target.dataset?.targethrefs) target = target.parentElement; - FormattedTextBoxComment.update(this, this.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); + 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'); } }; @action @@ -1613,7 +1609,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; @action onFocused = (e: React.FocusEvent): void => { - //applyDevTools.applyDevTools(this._editorView); + // 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(); }; @@ -1624,10 +1620,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e.stopPropagation(); return; } - if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) { - this._forceDownNode = undefined; - return; - } if (!this._forceUncollapse || (this._editorView!.root as any).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 }); @@ -1651,13 +1643,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (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, this._forceDownNode, e.shiftKey); + this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, e.shiftKey); } this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed; - this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node; }; // 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, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) { + hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, selectOrderedList: boolean = false) { this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this._editorView!.posAtCoords({ left: x, top: y }); @@ -1710,9 +1701,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB if (!(this.EditorView?.state.selection instanceof NodeSelection)) { this.autoLink(); if (this._editorView?.state.tr) { - const tr = stordMarks?.reduce((tr, m) => { - tr.addStoredMark(m); - return tr; + const tr = stordMarks?.reduce((tr2, m) => { + tr2.addStoredMark(m); + return tr2; }, this._editorView.state.tr); tr && this._editorView.dispatch(tr); } @@ -1727,6 +1718,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/); if (match) { this.dataDoc.title_custom = true; + // eslint-disable-next-line prefer-destructuring this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1)); } @@ -1736,33 +1728,31 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB FormattedTextBox.LiveTextUndo?.end(); FormattedTextBox.LiveTextUndo = undefined; - const state = this._editorView!.state; // if the text box blurs and none of its contents are focused(), then pass the blur along setTimeout(() => !this.ProseRef?.contains(document.activeElement) && this._props.onBlur?.()); }; onKeyDown = (e: React.KeyboardEvent) => { + const { _editorView } = this; + if (!_editorView) return; if ((e.altKey || e.ctrlKey) && e.key === 't') { - e.preventDefault(); - e.stopPropagation(); this._props.setTitleFocus?.(); + StopEvent(e); return; } - const state = this._editorView!.state; + const { state } = _editorView; if (!state.selection.empty && e.key === '%') { this._rules!.EnteringStyle = true; - e.preventDefault(); - e.stopPropagation(); + StopEvent(e); return; } if (state.selection.empty || !this._rules!.EnteringStyle) { this._rules!.EnteringStyle = false; } - let stopPropagation = true; - for (var i = state.selection.from; i <= state.selection.to; i++) { + for (let i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); - if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { + if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== ClientUtils.CurrentUserEmail()) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } @@ -1770,27 +1760,27 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB case 'Escape': this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as any).blur?.(); - SelectionManager.DeselectAll(); + DocumentView.DeselectAll(); RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); return; case 'Enter': this.insertTime(); + // eslint-disable-next-line no-fallthrough case 'Tab': e.preventDefault(); break; - case 'c': - this._editorView?.state.selection.empty && (stopPropagation = false); + case 'Space': + case 'Backspace': break; default: - if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; - case ' ': - if (e.code !== 'Space' && e.code !== 'Backspace') { - [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) && - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); + if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) { + const modified = Math.floor(Date.now() / 1000); + const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark && m.attrs.modified === modified); + _editorView.dispatch(state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail(), modified }))); } break; } - if (stopPropagation) e.stopPropagation(); + e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { @@ -1810,6 +1800,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children && !SnappingManager.IsDragging) { + // eslint-disable-next-line no-use-before-define const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { @@ -1821,7 +1812,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB const scrollHeight = this.ProseRef && proseHeight; if (this._props.setHeight && !this._props.suppressSetHeight && scrollHeight && !this._props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => (this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); + const setScrollHeight = () => { + this.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight; + }; if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); @@ -1839,7 +1832,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); - setSidebarHeight = (height: number) => (this.dataDoc[this.SidebarKey + '_height'] = height); + setSidebarHeight = (height: number) => { + this.dataDoc[this.SidebarKey + '_height'] = height; + }; sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props @@ -1857,10 +1852,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB e, returnFalse, emptyFunction, - action(e => (this._recordingDictation = !this._recordingDictation)) + action(() => { + this._recordingDictation = !this._recordingDictation; + }) ) }> - <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" /> + <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon="microphone" size="sm" /> </div> ); } @@ -1885,15 +1882,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } @computed get sidebarCollection() { const renderComponent = (tag: string) => { - const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; + const ComponentTag: any = tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView; return ComponentTag === CollectionStackingView ? ( <SidebarAnnos ref={this._sidebarRef} + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={this.Document} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} - usePanelWidth={true} + usePanelWidth nativeWidth={NumCast(this.layoutDoc._nativeWidth)} showSidebar={this.SidebarShown} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} @@ -1906,8 +1904,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB setHeight={this.setSidebarHeight} /> ) : ( - <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}> + <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.()!, false), true)}> <ComponentTag + // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} ref={this._sidebarTagRef as any} setContentView={emptyFunction} @@ -1930,8 +1929,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB renderDepth={this._props.renderDepth + 1} setHeight={this.setSidebarHeight} fitContentsToBox={this.fitContentsToBox} - noSidebar={true} - treeViewHideTitle={true} + noSidebar + treeViewHideTitle fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> @@ -1969,7 +1968,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }> <div className="formattedTextBox-alternateButton" - onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())} + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, () => this.cycleAlternateText())} style={{ display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none', background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray', @@ -1990,23 +1989,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB @observable _isHovering = false; onPassiveWheel = (e: WheelEvent) => { if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { - if (this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) { - // if the scrolled freeform is a child of the sidebar component, we need to let the event go through - // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling - e.preventDefault(); - } return; } // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) if (this._props.isContentActive()) { const scale = this._props.NativeDimScaling?.() || 1; - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > - const height = Number(styleFromLayoutString.height?.replace('px', '')); + const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > + const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( - !Number.isNaN(height) && - (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && // + !isNaN(height) && + (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height || this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); @@ -2016,7 +2010,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB }; _oldWheel: any; @computed get fontColor() { - return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color); + return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontColor); } @computed get fontSize() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize); @@ -2029,20 +2023,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB } render() { TraceMobx(); - const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1); + const scale = this._props.NativeDimScaling?.() || 1; const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0); const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0); - const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > - return styleFromLayoutString?.height === '0px' ? null : ( + const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' > + return styleFromLayout?.height === '0px' ? null : ( <div className="formattedTextBox" onPointerEnter={action(() => { this._isHovering = true; this.layoutDoc[`_${this._props.fieldKey}_usePath`] && (this.Document.isHovering = true); })} - onPointerLeave={action(() => (this.Document.isHovering = this._isHovering = false))} + onPointerLeave={action(() => { this.Document.isHovering = this._isHovering = false; })} // prettier-ignore ref={r => { this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); this._oldWheel = r; @@ -2062,7 +2056,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB fontSize: this.fontSize, fontFamily: this.fontFamily, fontWeight: this.fontWeight, - ...styleFromLayoutString, + ...styleFromLayout, }}> <div className="formattedTextBox-cont" @@ -2071,7 +2065,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB cursor: this._props.isContentActive() ? 'text' : undefined, height: this._props.height ? 'max-content' : undefined, overflow: this.layout_autoHeight ? 'hidden' : undefined, - pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none', + pointerEvents: Doc.ActiveTool === InkTool.None && !SnappingManager.ExploreMode ? undefined : 'none', }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyDown} @@ -2084,7 +2078,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB onDoubleClick={this.onDoubleClick}> <div className="formattedTextBox-outer" - ref={r => (this._scrollRef = r)} + ref={r => { + this._scrollRef = r; + }} style={{ width: this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, @@ -2112,3 +2108,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB ); } } + +Docs.Prototypes.TemplateMap.set(DocumentType.RTF, { + layout: { view: FormattedTextBox, dataField: 'text' }, + options: { + acl: '', + _height: 35, + _xMargin: 10, + _yMargin: 10, + _layout_nativeDimEditable: true, + _layout_reflowVertical: true, + _layout_reflowHorizontal: true, + defaultDoubleClick: 'ignore', + systemIcon: 'BsFileEarmarkTextFill', + }, +}); |