/* eslint-disable no-use-before-define */ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { Property } from 'csstype'; import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { baseKeymap, selectAll, splitBlock } from 'prosemirror-commands'; import { history } from 'prosemirror-history'; 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, NodeViewConstructor } from 'prosemirror-view'; import * as React from 'react'; import { BsMarkdownFill } from 'react-icons/bs'; import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, ClientUtils, DivWidth, removeStyleSheet, returnFalse, returnTrue, 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, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; 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 { emptyFunction, numberRange, unimplementedFunction, Utils } from '../../../../Utils'; import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; import { DocServer } from '../../../DocServer'; import { Docs } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocUtils } from '../../../documents/DocUtils'; import { DictationManager } from '../../../util/DictationManager'; import { DragManager } from '../../../util/DragManager'; import { dropActionType } from '../../../util/DropActionTypes'; import { LinkManager } from '../../../util/LinkManager'; import { RTFMarkup } from '../../../util/RTFMarkup'; import { SnappingManager } from '../../../util/SnappingManager'; import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionStackingView } from '../../collections/CollectionStackingView'; import { CollectionTreeView } from '../../collections/CollectionTreeView'; import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { AnchorMenu } from '../../pdf/AnchorMenu'; import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup'; import { PinDocView, PinProps } from '../../PinFuncs'; import { SidebarAnnos } from '../../SidebarAnnos'; import { StickerPalette } from '../../smartdraw/StickerPalette'; 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 { LabelBox } from '../LabelBox'; import { LinkInfo } from '../LinkDocPreview'; import { OpenWhere } from '../OpenWhere'; import './FormattedTextBox.scss'; import { findLinkMark, FormattedTextBoxComment } from './FormattedTextBoxComment'; import { buildKeymap, updateBullets } from './ProsemirrorExampleTransfer'; import { removeMarkWithAttrs } from './prosemirrorPatches'; import { RichTextMenu, RichTextMenuPlugin } from './RichTextMenu'; import { RichTextRules } from './RichTextRules'; import { schema } from './schema_rts'; // import * as applyDevTools from 'prosemirror-dev-tools'; 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() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static MakeConfig(rules?: RichTextRules, textBox?: FormattedTextBox, plugs?: Plugin[]) { return { schema, plugins: [ inputRules(rules?.inpRules ?? { rules: [] }), ...(textBox?._props ? [FormattedTextBox.richTextMenuPlugin(textBox._props)] : []), history(), keymap(buildKeymap(schema, textBox)), keymap(baseKeymap), new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }), new Plugin({ view: () => new FormattedTextBoxComment() }), ...(plugs ?? []), ], }; } /** * 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; } // prettier-ignore public static PasteOnLoad: ClipboardEvent | undefined; public static SelectOnLoadChar = ''; public static LiveTextUndo: UndoManager.Batch | undefined; // undo batch request when typing a new text note into a collection private _liveTextUndo: UndoManager.Batch | undefined; // captured undo batch when typing a new text note into a collection private static _nodeViews: (self: FormattedTextBox) => { [key: string]: NodeViewConstructor }; private _curHighlights = new ObservableSet(['Audio Tags']); private static _highlightStyleSheet = addStyleSheet().sheet; private static _bulletStyleSheet = addStyleSheet().sheet; private _userStyleSheetElement: HTMLStyleElement | undefined; private _enteringStyle = false; private _oldWheel: HTMLDivElement | null = null; private _selectionHTML: string | undefined; private _sidebarRef = React.createRef(); private _sidebarTagRef = React.createRef(); private _ref: React.RefObject = React.createRef(); private _scrollRef: HTMLDivElement | null = null; private _editorView: Opt; private _inDrop = false; private _finishingLink = false; private _searchIndex = 0; private _cachedLinks: Doc[] = []; private _undoTyping?: UndoManager.Batch; private _disposers: { [name: string]: IReactionDisposer } = {}; private _dropDisposer?: DragManager.DragDropDisposer; private _recordingStart: number = 0; private _ignoreScroll = false; private _focusSpeed: Opt; 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; /** * ApplyingChange - Marks whether an interactive text edit is currently in the process of being written to the database. * This is needed to distinguish changes to text fields caused by editing vs those caused by changes to * the prototype or other external edits */ public ApplyingChange: string = ''; @observable _showSidebar = false; @observable _userPlugins: Plugin[] = []; @computed get fontColor() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontColor) as string; } // prettier-ignore @computed get fontSize() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontSize) as string; } // prettier-ignore @computed get fontFamily() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontFamily) as string; } // prettier-ignore @computed get fontWeight() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontWeight) as string; } // prettier-ignore @computed get fontStyle() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontStyle) as string; } // prettier-ignore @computed get fontDecoration() { return this._props.styleProvider?.(this.dataDoc, this._props, StyleProp.FontDecoration) as string; } // prettier-ignore set recordingDictation(value) { !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined); } // eslint-disable-next-line no-return-assign @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this, this._userPlugins ?? []); } // prettier-ignore @computed get recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore @computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // 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 sidebarWidthPercent() { return StrCast(this.layoutDoc._layout_sidebarWidthPercent, this._showSidebar ? '20%' :'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.layoutDoc["_"+this.fieldKey + '_height']); } // prettier-ignore @computed get scrollHeight() { return NumCast(this.layoutDoc["_"+this.fieldKey + '_scrollHeight']); } // prettier-ignore @computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.layoutDoc["_"+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 sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore @computed get isLabel() { return this.dataDoc[this.fieldKey+"_fitBox"]; } // prettier-ignore constructor(props: FormattedTextBoxProps) { super(props); makeObservable(this); this._recordingStart = Date.now(); } public get EditorView() { return this.isLabel ? undefined : this._editorView; } // prettier-ignore // public makeAIFlashcards: () => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; // removes all hyperlink anchors for the removed linkDoc // TODO: bcz: Argh... if a section of text has multiple anchors, this should just remove the intended one. // but since removing one anchor from the list of attr anchors isn't implemented, this will end up removing nothing. public RemoveLinkFromDoc(linkDoc?: Doc) { this.unhighlightSearchTerms(); const state = this.EditorView?.state; 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); 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; }); if (allFoundLinkAnchors.length) { this.EditorView.dispatch(removeMarkWithAttrs(state.tr, 0, state.doc.nodeSize - 2, state.schema.marks.linkAnchor, { allAnchors: allFoundLinkAnchors })); this.setupEditor(this.config, this.fieldKey); } } } // removes all the specified link references from the selection. // NOTE: as above, this won't work correctly if there are marks with overlapping but not exact sets of link references. public RemoveAnchorFromSelection(allAnchors: { href: string; title: string; linkId: string; targetId: string }[]) { const state = this.EditorView?.state; if (state && this.EditorView) { this.EditorView.dispatch(removeMarkWithAttrs(state.tr, state.selection.from, state.selection.to, state.schema.marks.link, { allAnchors })); this.setupEditor(this.config, this.fieldKey); } } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { if (!pinProps && this.EditorView?.state.selection.empty) return this.rootDoc; const anchor = Docs.Create.ConfigDocument({ title: StrCast(this.rootDoc?.title), annotationOn: this.rootDoc }); this.addDocument(anchor); this._finishingLink = true; this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); this._finishingLink = false; PinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document); return anchor; }; @action setupAnchorMenu = () => { AnchorMenu.Instance.Status = 'marquee'; // AnchorMenu.Instance.gptFlashcards = this.selectionToFlashcards; AnchorMenu.Instance.makeLabels = unimplementedFunction; AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; 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 = () => { !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); setTimeout(() => { const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; let stopFunc: () => void = emptyFunction; target.$mediaState = mediaState.Recording; DictationManager.recordAudioAnnotation(target, Doc.LayoutDataKey(target), stop => { stopFunc = stop }); // prettier-ignore const reactionDisposer = reaction( () => target.mediaState, dictation => { if (!dictation) { stopFunc(); reactionDisposer(); } } ); target.title = ComputedField.MakeFunction(`this.text_audioAnnotations_text.lastElement()`); } }); }; 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; /** * This function is used by the PDFmenu to create an anchor highlight and a new linked text annotation. * It also initiates a Drag/Drop interaction to place the text annotation. */ AnchorMenu.Instance.StartDrag = action(async (e: PointerEvent, ele: HTMLElement) => { e.preventDefault(); e.stopPropagation(); const targetCreator = (annotationOn?: Doc) => { const target = DocUtils.GetNewTextDoc('Note linked to ' + this.Document.title, 0, 0, 100, 100, annotationOn, 'yellow'); target.layout_fitWidth = true; DocumentView.SetSelectOnLoad(target); return target; }; const sourceAnchorCreator = () => this.getAnchor(true); const docView = this.DocumentView?.(); docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY); }); AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { const container = DocCast(this.Document.embedContainer); const docView = DocumentView.getDocumentView?.(container); docView?.ComponentView?._props.addDocument?.(drawing); drawing.x = NumCast(this.Document.x) + NumCast(this.Document.width); drawing.y = NumCast(this.Document.y); }; 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; try { const contents = window.getSelection()?.getRangeAt(0).cloneContents(); if (contents) { ele = document.createElement('div'); ele.append(contents); } this._selectionHTML = ele?.innerHTML; // eslint-disable-next-line @typescript-eslint/no-unused-vars } catch (e) { /* empty */ } }; autoTag = () => { const rawText = RTFCast(this.Document[this.fieldKey])?.Text ?? StrCast(this.Document[this.fieldKey]); if (rawText && !this.Document.$tags_chat) { const callType = rawText.includes('[placeholder]') ? GPTCallType.CLASSIFYTEXTMINIMAL : GPTCallType.CLASSIFYTEXTFULL; gptAPICall(rawText, callType).then( action(desc => { // Split GPT response into tokens and push individually & clear existing tags this.Document.$tags_chat = new List(desc.trim().split(/\s+/)); this.Document._layout_showTags = true; }) ); } }; leafText = (node: Node) => { if (node.type === this.EditorView?.state.schema.nodes.dashField) { const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); const fieldKey = StrCast(node.attrs.fieldKey); return ( (node.attrs.hideKey ? '' : fieldKey + ':') + // (node.attrs.hideValue ? '' : Field.toJavascriptString(refDoc[fieldKey] as FieldType)) ); } if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { const refDoc = !node.attrs.docId ? this.rootDoc : (DocServer.GetCachedRefField(node.attrs.docId as string) as Doc); return refDoc[ToString](); } return ''; }; dispatchTransaction = (tx: Transaction) => { if (this.EditorView && !this.EditorView.isDestroyed) { const state = this.EditorView.state.apply(tx); this.EditorView.updateState(state); this.tryUpdateDoc(false); } }; tryUpdateDoc = (force: boolean) => { 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 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); const removeSelection = (json: string | undefined) => json?.replace(/"selection":.*/, ''); if ([AclEdit, AclAdmin, AclSelfEdit, AclAugment].includes(effectiveAcl)) { const accumTags = [] as string[]; 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); } }); if (accumTags.some(atag => !StrListCast(dataDoc.tags).includes(atag))) { dataDoc.tags = new List(Array.from(new Set(accumTags))); } 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; if ((!prevData && !protoData && !layoutData) || newText || (!newText && !protoData && !layoutData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) if (force || ((this._finishingLink || this._props.isContentActive() || this._inDrop) && (textChange || removeSelection(newJson) !== removeSelection(prevData?.Data)))) { textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); textChange && (dataDoc[this.fieldKey + '_placeholder'] = undefined); const numstring = NumCast(dataDoc[this.fieldKey], null); dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : newText || (DocCast(dataDoc.proto)?.[this.fieldKey] === undefined && this.layoutDoc[this.fieldKey] === undefined) ? new RichTextField(newJson, newText) : undefined; textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.Document, text: newText }); if (textChange) this.dataDoc.$tags_chat = undefined; this.ApplyingChange = ''; // turning this off here allows a Doc to retrieve data from template if noTemplate below is changed to false unchanged = false; } } else if (rtField) { textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); // 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(rtField.Data))); ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, text: newText }); unchanged = false; } this.ApplyingChange = ''; if (!unchanged) { this.updateTitle(); this.tryUpdateScrollHeight(); } } } else { const jsonstring = Cast(dataDoc[this.fieldKey], RichTextField)?.Data; if (jsonstring) { const json = JSON.parse(jsonstring); json.selection = state.toJSON().selection; this.EditorView.updateState(EditorState.fromJSON(this.config, json)); } } if (window.getSelection()?.isCollapsed && this._props.rootSelected?.()) { AnchorMenu.Instance.fadeOut(true); } } }; // for inserting timestamps insertTime = () => { let linkTime; let linkAnchor; 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; } }); if (this.EditorView && linkTime) { const { state } = this.EditorView; 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; 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)))); } } }; autoLink = () => { const newAutoLinks = new Set(); const oldAutoLinks = Doc.Links(this.Document).filter( link => ((!Doc.IsTemplateForField(this.Document) && ((DocCast(link.link_anchor_1) && !Doc.IsTemplateForField(DocCast(link.link_anchor_1)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) && ((DocCast(link.link_anchor_2) && !Doc.IsTemplateForField(DocCast(link.link_anchor_2)!)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_2), this.Document))) || (Doc.IsTemplateForField(this.Document) && (link.link_anchor_1 === this.Document || link.link_anchor_2 === this.Document))) && link.link_relationship === LinkManager.AutoKeywords ); // prettier-ignore if (this.EditorView?.state.doc.textContent) { 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); }); const marks = tr.storedMarks; tr = tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to))).setStoredMarks(marks); this.EditorView?.dispatch(tr); } oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(doc => Doc.DeleteLink?.(doc)); }; updateTitle = () => { const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text); if ( !this._props.dontRegisterView && // only update the title if the data document's data field is changing title.startsWith('-') && this.EditorView && !this.dataDoc.title_custom && (Doc.LayoutDataKey(this.Document) === this.fieldKey || this.fieldKey === 'text') ) { let node = this.EditorView.state.doc; while (node.firstChild && node.firstChild.type.name !== 'text') node = node.firstChild; const str = node.textContent; const prefix = '-'; const cfield = ComputedField.DisableCompute(() => FieldValue(this.dataDoc.title)); if (!(cfield instanceof ComputedField)) { this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim(); } } }; // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@' /** * Searches the text for occurences of any strings that match the names of 'published' documents. These document * names will begin with an '@' prefix. However, valid matches within the text can have any of the following formats: * name, @, or ^@ * The last of these is interpreted as an include directive when converting the text into evaluated code in the paint * 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: Transaction, target: Doc, newAutoLinks: Set) => { let tr = trIn; const editorView = this.EditorView; 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 || autoLinkTerm === editorView.state.doc .textBetween(sel.$anchor.pos - 1, sel.$to.pos) .trim() .replace(/[\^@]+/, '') ) { 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: 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 ?? (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) ) || DocUtils.MakeLink(this.Document, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); // DocCast(alink.link_anchor_1).followLinkLocation = 'add:right'; const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); const link = editorView.state.schema.marks.autoLinkAnchor.create({ allAnchors, title: 'auto term' }); tr = tr.addMark(pos, pos + node.nodeSize, link); } }); tr = tr.removeMark(sel.from, sel.to, splitter); } }); } return tr; }; @action search = (searchString: string, bwd?: boolean, clear: boolean = false) => { if (clear) this.unhighlightSearchTerms(); else this.highlightSearchTerms([searchString], bwd!); return true; }; highlightSearchTerms = (terms: string[], backward: boolean) => { 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; if (backward === true) { if (this._searchIndex > 1) { this._searchIndex += -2; } else if (this._searchIndex === 1) { this._searchIndex = length - 1; } else if (this._searchIndex === 0 && length !== 1) { this._searchIndex = length - 2; } } 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] && _editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); } }; unhighlightSearchTerms = () => { 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: ClientUtils.CurrentUserEmail() }); view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark)); }; protected createDropTarget = (ele: HTMLDivElement) => { this._dropDisposer?.(); this.ProseRef = ele; if (ele) { this.setupEditor(this.config, this.fieldKey); this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); } // if (this.layout_autoHeight) this.tryUpdateScrollHeight(); }; @undoBatch drop = (e: Event, de: DragManager.DropEvent) => { if (de.complete.annoDragData) { de.complete.annoDragData.dropDocCreator = () => this.getAnchor(true); return true; } const dragData = de.complete.docDragData; if (dragData && !this._props.rejectDrop?.(de, this.DocumentView?.())) { const layoutProto = DocCast(this.layoutDoc.proto); const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const effectiveAcl = GetEffectiveAcl(dataDoc); const draggedDoc = dragData.droppedDocuments.lastElement(); let added: Opt; const dropAction = dragData.dropAction || dragData.userDropAction; if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl) && !dragData.draggedDocuments.includes(this.Document)) { // replace text contents when dragging with Alt if (de.altKey) { const fieldKey = Doc.LayoutDataKey(draggedDoc); if (draggedDoc[fieldKey] instanceof RichTextField && !Doc.AreProtosEqual(draggedDoc, this.Document)) { Doc.GetProto(this.dataDoc)[this.fieldKey] = Field.Copy(draggedDoc[fieldKey]); } // embed document when drag marked as embed } else if (de.embedKey || dropAction) { const node = schema.nodes.dashDoc.create({ width: NumCast(draggedDoc._width), height: NumCast(draggedDoc._height), title: 'dashDoc', docId: draggedDoc[Id], float: 'unset', }); if (!de.embedKey && ![dropActionType.embed, dropActionType.copy].includes(dropAction ?? dropActionType.move)) { added = !!dragData.removeDocument?.(draggedDoc); } else { added = true; } if (added) { draggedDoc._freeform_fitContentsToBox = true; Doc.SetContainer(draggedDoc, this.Document); const view = this.EditorView!; try { 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; // 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; } } } } // otherwise, fall through to outer collection to handle drop added === false && e.preventDefault(); added === true && e.stopPropagation(); return added; } return false; }; getNodeEndpoints(context: Node, node: Node): { from: number; to: number } | null { let offset = 0; if (context === node) return { from: offset, to: offset + node.nodeSize }; if (node.isBlock) { // tslint:disable-next-line: prefer-for-of 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.child(i).nodeSize; } } return null; } // Recursively finds matches within a given node findInNode(pm: EditorView, node: Node, find: string) { let ret: TextSelection[] = []; if (node.isTextblock) { let index = 0; let foundAt; const ep = this.getNodeEndpoints(pm.state.doc, node); const regexp = new RegExp(find, 'i'); if (regexp) { 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++; } 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); index = index + foundAt + find.length; } blockOffset += textContent.length; if (i < node.childCount) blockOffset += node.child(i).nodeSize; } } } else { node.content.forEach(child => { ret = ret.concat(this.findInNode(pm, child, find)); }); } return ret; } updateHighlights = (highlights: string[]) => { const userStyleSheet = () => { if (!this._userStyleSheetElement) { this._userStyleSheetElement = addStyleSheet(); } return this._userStyleSheetElement.sheet; }; const viewId = this.DocumentView?.().ViewGuid ?? 1; const userId = ClientUtils.CurrentUserEmail().replace(/\./g, '').replace('@', ''); // must match marks_rts -> user_mark's uid highlights.filter(f => f !== 'Audio Tags').length && clearStyleSheetRules(userStyleSheet()); if (!highlights.includes('Audio Tags')) addStyleSheetRule(userStyleSheet(), `#${viewId} .audiotag`, { display: 'none' }, ''); // prettier-ignore if (highlights.includes('Text from Others')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-remote`, { background: 'yellow' }, ''); // prettier-ignore if (highlights.includes('My Text')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { background: 'moccasin' }, ''); // prettier-ignore if (highlights.includes('Todo Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-todo`, { outline: 'black solid 1px' }, ''); // prettier-ignore if (highlights.includes('Important Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-important`, { 'font-size': 'larger' }, ''); // prettier-ignore if (highlights.includes('Disagree Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-disagree`, { 'text-decoration': 'line-through' }, ''); // prettier-ignore if (highlights.includes('Ignore Items')) addStyleSheetRule(userStyleSheet(), `#${viewId} .UT-ignore`, { 'font-size': '1' }, ''); // prettier-ignore if (highlights.includes('Bold Text')) { addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong))`, { 'font-size': '0px' }, ''); addStyleSheetRule(userStyleSheet(), `#${viewId} .formattedTextBox-inner .ProseMirror p:not(:has(strong)) ::after`, { content: '...', 'font-size': '5px' }, '')} // prettier-ignore if (highlights.includes('By Recent Minute')) { addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-min-` + (min - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } if (highlights.includes('By Recent Hour')) { addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-${userId}`, { opacity: '0.1' }, ''); const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(userStyleSheet(), `#${viewId} .UM-hr-` + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }, '')); } 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) }; @action toggleSidebar = (preview: boolean = false) => { const defaultSidebar = 250; const dw = DivWidth(this._ref.current); const prevWidth = 1 - this.sidebarWidth() / dw / this.nativeScaling(); if (preview) this._showSidebar = true; else { this.layoutDoc._layout_sidebarWidthPercent = this.sidebarWidthPercent === '0%' // ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` // : '0%'; this.layoutDoc._layout_showSidebar = this.sidebarWidthPercent !== '0%'; } this.layoutDoc._width = !preview && this.SidebarShown // ? NumCast(this.layoutDoc._width) + defaultSidebar : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { const batch = UndoManager.StartBatch('toggle sidebar'); setupMoveUpEvents( this, e, this.sidebarMove, (moveEv, movement, isClick) => !isClick && batch.end(), () => { this.toggleSidebar(); batch.end(); }, true ); }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const localDelta = this.DocumentView?.().screenToViewTransform().transformDirection(delta[0], delta[1]) ?? delta; const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; const width = NumCast(this.layoutDoc._width) + localDelta[0]; this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; e.preventDefault(); return false; }; deleteAnnotation = (anchor: Doc) => { const batch = UndoManager.StartBatch('delete link'); Doc.DeleteLink?.(Doc.Links(anchor)[0]); // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]); // this._props.dataDoc[this.fieldKey] = new List(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); this._props.select(false); setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch pinToPres = (anchor: Doc) => this._props.pinToPres(anchor, {}); @undoBatch makeTargetToggle = (anchor: Doc) => { anchor.followLinkToggle = !anchor.followLinkToggle; }; @undoBatch showTargetTrail = (anchor: Doc) => { const trail = DocCast(anchor.presentationTrail); if (trail) { Doc.ActivePresentation = trail; this._props.addDocTab(trail, OpenWhere.replaceRight); } }; isTargetToggler = (anchor: Doc) => BoolCast(anchor.followLinkToggle); specificContextMenu = (e: React.MouseEvent): void => { if (this._props.dontSelect?.()) return; const cm = ContextMenu.Instance; let target: Element | HTMLElement | null = e.target as HTMLElement; // hrefs are stored on the database of the node that wraps the hyerlink while (target && (!(target instanceof HTMLElement) || !target.dataset?.targethrefs)) target = target.parentElement; const editor = this.EditorView; if (editor && target && !(e.nativeEvent instanceof simMouseEvent ? e.nativeEvent.dash : false)) { const hrefs = (target.dataset?.targethrefs as string) ?.trim() .split(' ') .filter(h => h); const anchorDoc = Array.from(hrefs ?? []) .lastElement() .replace(Doc.localServerPath(), '') .split('?')[0]; 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( action(anchor => { 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; AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc); AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc); AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc); AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); }) ); e.preventDefault(); e.stopPropagation(); return; } const highlighting: ContextMenuProps[] = []; const noviceHighlighting = ['Audio Tags', 'My Text', 'Text from Others', 'Bold Text']; const expertHighlighting = [...noviceHighlighting, 'Important Items', 'Ignore Items', 'Disagree Items', 'By Recent Minute', 'By Recent Hour']; (Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ description: (!this._curHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option, event: action(() => { e.stopPropagation(); if (!this._curHighlights.has(option)) { this._curHighlights.add(option); } else { this._curHighlights.delete(option); } }), icon: !this._curHighlights.has(option) ? 'highlighter' : 'remove-format', }) ); const appearance = cm.findByDescription('Appearance...'); const appearanceItems = appearance?.subitems ?? []; // appearanceItems.push({ // description: 'Find image tags', // event: this.findImageTags, // icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', // }); appearanceItems.push({ description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', 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; }, icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); if (this.Document._layout_enableAltContentUI) { const usepath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; appearanceItems.push({ description: (this.layoutDoc[`_${this._props.fieldKey}_usePath`] === 'alternate:hover' ? 'no hover' : 'hover') + ' to show alt content', event: () => { this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usepath === 'alternate' || usepath === undefined ? 'alternate:hover' : undefined; }, 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); }), icon: 'expand-arrows-alt', }); !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); const options = cm.findByDescription('Options...'); const optionItems = 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: `Generate Dall-E Image`, event: this.generateImage, icon: 'star' }); // optionItems.push({ description: `Make AI Flashcards`, event: () => this.makeAIFlashcards(), icon: 'lightbulb' }); 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; }, icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); optionItems.push({ description: this.Document.savedAsSticker ? 'Sticker Saved!' : 'Save to Stickers', event: action(undoable(async () => await StickerPalette.addToPalette(this.Document), 'save to palette')), icon: this.Document.savedAsSticker ? 'clipboard-check' : 'file-arrow-down', }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const help = cm.findByDescription('Help...'); const helpItems = help?.subitems ?? []; helpItems.push({ description: `show markdown options`, event: () => RTFMarkup.Instance.setOpen(true), icon: }); !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' }); }; animateRes = (resIndex: number, newText: string) => { if (resIndex < newText.length) { const marks = this.EditorView?.state.storedMarks ?? []; this.EditorView?.dispatch(this.EditorView?.state.tr.insertText(newText[resIndex]).setStoredMarks(marks)); setTimeout(() => this.animateRes(resIndex + 1, newText), 20); } }; askGPT = action(async () => { try { GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; const res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION); if (!res) { this.animateRes(0, 'Something went wrong.'); } else if (this.EditorView) { const { dispatch, state } = this.EditorView; // for no animation, use: dispatch(state.tr.insertText(res)); // for animted response starting at end of text, use: dispatch(state.tr.setSelection(Selection.atEnd(state.doc))); this.animateRes(0, '\n\n' + res); } } catch (err) { console.error(err); this.animateRes(0, 'Something went wrong.'); } }); generateImage = () => { GPTPopup.Instance?.setTextAnchor(this.getAnchor(false)); GPTPopup.Instance.generateImage((this.dataDoc.text as RichTextField)?.Text, this.Document, this._props.addDocument); }; breakupDictation = () => { if (this.EditorView && this.recordingDictation) { this.stopDictation(/* true */); this._break = true; 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) { this.recordDictation(); } } }; recordDictation = () => { DictationManager.Controls.listen({ interimHandler: this.setDictationContent, continuous: { indefinite: false }, }).then(results => { if (results && [DictationManager.Controls.Infringed].includes(results)) { DictationManager.Controls.stop(); } }); }; stopDictation = (/* abort: boolean */) => DictationManager.Controls.stop(/* !abort */); setDictationContent = (value: string) => { if (this.EditorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { const tanch = Docs.Create.ConfigDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; }; const link = CreateLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { link.$isDictation = true; const audioanchor = DocCast(link.link_anchor_2); const textanchor = DocCast(link.link_anchor_1); if (audioanchor) { audioanchor.backgroundColor = 'tan'; const audiotag = this.EditorView.state.schema.nodes.audiotag.create({ timeCode: NumCast(audioanchor._timecodeToShow), audioId: audioanchor[Id], textId: textanchor?.[Id] ?? '', }); textanchor && (textanchor.$title = 'dictation:' + audiotag.attrs.timeCode); const tr = this.EditorView.state.tr.insert(this.EditorView.state.doc.content.size, audiotag); const tr2 = tr.setSelection(TextSelection.create(tr.doc, tr.doc.content.size)); this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr2.doc, tr2.doc.content.size))); } } } 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()); } }; // TODO: nda -- Look at how link anchors are added makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string, noPreview?: boolean, addAsAnnotation?: boolean) { const { _editorView } = this; if (_editorView) { const { state } = _editorView; let selectedText = ''; const { selection } = state; const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(selection.from, selection.to, splitter); if (selection.from !== selection.to) { const anchor = anchorDoc ?? Docs.Create.ConfigDocument({ // 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(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 ?? [])); const link = state.schema.marks.linkAnchor.create({ allAnchors, title, location, noPreview }); tr = tr.addMark(pos, pos + node.nodeSize, link); selectedText += (node as Node).textContent; } }); this.dataDoc[ForceServerWrite] = this.dataDoc[UpdatingFromServer] = true; // need to allow permissions for adding links to readonly/augment only documents 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; anchor.title = selectedText.substring(0, 30); anchor.presentation_zoomText = true; return anchor; } return anchorDoc ?? this.Document; } return anchorDoc ?? this.Document; } showBorderRounding = returnTrue; getView = (doc: Doc, options: FocusViewOptions) => { if (DocListCast(this.dataDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { return SidebarAnnos.getView(this._sidebarRef.current, this.SidebarShown, () => this.toggleSidebar(false), doc, options); } return new Promise>(res => DocumentView.addViewRenderedCb(doc, res)); }; 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) => { 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); !hadStart && (start = index + examinedNode.start); hadStart = true; } }); return { frag: Fragment.fromArray(nodes), start }; }; const findAnchorNode = (node: Node, editor: EditorView) => { if (node.type === this.EditorView?.state.schema.nodes.audiotag) { if (node.attrs.textId === textAnchorId) { return { node, start: 0 }; } return undefined; } if (node.type === this.EditorView?.state.schema.nodes.dashDoc) { if (node.attrs.docId === textAnchorId) { return { node, start: 0 }; } return undefined; } if (!node.isText) { const content = findAnchorFrag(node.content, editor); if (content.frag.childCount) return { node: content.frag.childCount ? content.frag.child(0) : node, start: content.start }; } const marks = [...node.marks]; const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.linkAnchor); return linkIndex !== -1 && marks[linkIndex].attrs.allAnchors.find((item: { href: string }) => textAnchorId === item.href.replace(/.*\/doc\//, '')) ? { node, start: 0 } : undefined; }; 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 { state } = this.EditorView; const ret = findAnchorFrag(state.doc.content, this.EditorView); 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(state.doc.resolve(ret.start)); // default to near the start if (ret.frag.firstChild) { 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 } 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(() => { this._focusSpeed = undefined; }, this._focusSpeed); setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000)); return focusSpeed; } return this._props.focus(this.Document, options); } return undefined; }; addPlugin = (plugin: Plugin) => { const editorView = this.EditorView; if (editorView) { this._userPlugins.push(plugin); const newState = editorView.state.reconfigure({ plugins: [...editorView.state.plugins, plugin], }); editorView.updateState(newState); } }; @computed get tagsHeight() { return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yMargin ?? 0, NumCast(this.layoutDoc._yMargin))) * this.ScreenToLocalBoxXf().Scale : 0; } @computed get contentScaling() { return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this.nativeScaling() : 1; } 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 = 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], xMargin: this.Document.xMargin, yMargin: this.Document.yMargin }), autoHeight => setTimeout(() => autoHeight && this.tryUpdateScrollHeight()) ); this._disposers.highlights = reaction(() => Array.from(this._curHighlights).slice(), this.updateHighlights, { fireImmediately: true }); this._disposers.width = reaction(this._props.PanelWidth, this.tryUpdateScrollHeight); this._disposers.scrollHeight = reaction( () => ({ scrollHeight: this.scrollHeight, layoutAutoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), ({ width, scrollHeight, layoutAutoHeight }) => width && layoutAutoHeight && (this.layoutDoc['_' + this.fieldKey + '_height'] = scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and layout_autoHeight is on () => ({ border: this._props.PanelHeight(), scrollHeight: NumCast(this.layoutDoc['_' + this.fieldKey + '_height']), sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins, }), ({ border, sidebarHeight, scrollHeight, textHeight, layoutAutoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); if ( (!Array.from(this._curHighlights).includes('Bold Text') || this._props.isSelected()) && // layoutAutoHeight && newHeight && (newHeight !== this.layoutDoc.height || border < NumCast(this.layoutDoc.height) || this.layoutDoc._nativeHeight !== scrollHeight) && !this._props.dontRegisterView ) { if (NumCast(this.layoutDoc.nativeHeight)) { this.layoutDoc._nativeHeight = scrollHeight; } this._props.setHeight?.(newHeight); } }, { fireImmediately: !Array.from(this._curHighlights).includes('Bold Text') } ); this._disposers.links = reaction( () => 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; } ); this._disposers.editorState = reaction( () => { 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 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) ?? NumCast(whichData)?.toString() ?? StrCast(whichData)) }; }, incomingValue => { if (this.EditorView && this.ApplyingChange !== this.fieldKey) { if (incomingValue?.data) { const updatedState = JSON.parse(incomingValue.data.Data.replace(/\n/g, '')); if (JSON.stringify(this.EditorView.state.toJSON()) !== JSON.stringify(updatedState)) { this.EditorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateScrollHeight(); } } else if (this.EditorView.state.doc.textContent !== (incomingValue?.str ?? '')) { selectAll(this.EditorView.state, tx => this.EditorView?.dispatch(tx.insertText(incomingValue?.str ?? ''))); this.tryUpdateScrollHeight(); } } }, { fireImmediately: true } ); this._disposers.search = reaction( () => Doc.IsSearchMatch(this.Document), search => (search ? this.highlightSearchTerms([Doc.SearchQuery()], search.searchMatch < 0) : this.unhighlightSearchTerms()), { fireImmediately: !!Doc.IsSearchMatchUnmemoized(this.Document) } ); this._disposers.selected = reaction( () => this._props.rootSelected?.() || this._props.isContentActive(), action(selected => { if (selected && this.dataDoc[this.fieldKey + '_placeholder']) { setTimeout(() => { selectAll(this.EditorView!.state, (tx: Transaction) => { this.EditorView?.dispatch(tx); this.EditorView!.focus(); }); }); } this.prepareForTyping(); if (this._curHighlights.has('Bold Text')) { 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 && this.EditorView) || this.isLabel) && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } if (selected) { RichTextMenu.Instance?.updateMenu(this.EditorView, undefined, this._props, this.dataDoc); this.EditorView && setTimeout(this.autoLink, 20); } }), { fireImmediately: true } ); if (!this._props.dontRegisterView) { this._disposers.record = reaction( () => this.recordingDictation, () => { this.stopDictation(/* true */); this.recordingDictation && this.recordDictation(); }, { fireImmediately: true } ); if (this.recordingDictation) setTimeout(this.recordDictation); } this._disposers.scroll = reaction( () => NumCast(this.layoutDoc._layout_scrollTop), pos => { if (!this._ignoreScroll && this._scrollRef) { const durationStr = StrCast(this.Document._viewTransition).match(/([0-9]+)(m?)s/); const duration = Number(durationStr?.[1]) * (durationStr?.[2] ? 1 : 1000); this._scrollStopper = smoothScroll(duration || 0, this._scrollRef, Math.abs(pos || 0), 'ease', this._scrollStopper); } }, { fireImmediately: true } ); this.tryUpdateScrollHeight(); if (this.Document.image) { // const node = schema.nodes.dashDoc.create({ // width: 200, // height: 200, // title: 'dashDoc', // docId: DocCast(this.Document.image)[Id], // float: 'unset', // }); // DocCast(this.Document.image)._freeform_fitContentsToBox = true; // Doc.SetContainer(DocCast(this.Document.image), this.Document); // const view = this.EditorView!; // try { // this._inDrop = true; // const pos = view.posAtCoords({ left: 0, top: 0 })?.pos; // pos && view.dispatch(view.state.tr.insert(pos, node)); // } catch (err) { // console.log('Drop failed', err); // } DocCast(this.Document.image) && this.addDocument?.(DocCast(this.Document.image)!); } //if (this.Document.image) this.addDocument?.(DocCast(this.Document.image)); setTimeout(this.tryUpdateScrollHeight, 250); AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; } clipboardTextSerializer = (slice: Slice): string => { let text = ''; let separated = true; const from = 0; const to = slice.content.size; slice.content.nodesBetween( from, to, (node, pos) => { if (node.isText) { text += node.text!.slice(Math.max(from, pos) - pos, to - pos); separated = false; } else if (!separated && node.isBlock) { text += '\n'; separated = true; } else if (node.type.name === schema.nodes.hard_break.name) { text += '\n'; } }, 0 ); return text; }; handlePaste = (view: EditorView, event: ClipboardEvent /* , slice: Slice */): boolean => { return this.doPaste(view, event.clipboardData); }; doPaste = (view: EditorView, data: DataTransfer | null) => { const html = data?.getData('text/html'); const pdfAnchorId = data?.getData('dash/pdfAnchor'); if (html && !pdfAnchorId) { const replaceDivsWithParagraphs = (expr: string) => { // Create a temporary DOM container const container = document.createElement('div'); container.innerHTML = expr; // Recursive function to process all divs function processDivs(node: HTMLElement) { // Get all div elements in the current node (live collection) const divs = node.getElementsByTagName('div'); // We need to convert to array because we'll be modifying the DOM const divsArray = Array.from(divs); for (const div of divsArray) { // Create replacement paragraph const p = document.createElement('p'); // Copy all attributes for (const attr of div.attributes) { p.setAttribute(attr.name, attr.value); } // Move all child nodes while (div.firstChild) { p.appendChild(div.firstChild); } // Replace the div with the paragraph div.parentNode?.replaceChild(p, div); // Process any nested divs that were moved into the new paragraph processDivs(p); } } // Start processing from the container processDivs(container); return container.innerHTML; }; const fixedHTML = replaceDivsWithParagraphs(html); // .replace(/]*)>(.*?)<\/div>/g, '$2

'); // prettier-ignore this._inDrop = true; view.pasteHTML(html.split(' { const view = this.EditorView!; if (pdfAnchorId) { DocServer.GetRefField(pdfAnchorId).then(pdfAnchor => { if (pdfAnchor instanceof Doc) { const dashField = view.state.schema.nodes.paragraph.create({}, [ view.state.schema.nodes.dashField.create({ fieldKey: 'text', docId: pdfAnchor[Id], hideKey: true, hideValue: false, editable: false }, undefined, [ view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.Document[Id]}`, title: this.Document.title, anchorId: `${this.Document[Id]}` }], title: StrCast(pdfAnchor.title), noPreview: true, docref: true, fontSize: '8px', }), ]), ]); const link = DocUtils.MakeLink(pdfAnchor, this.Document, { link_relationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } } }); return true; } return false; }; isActiveTab(elIn: Element | null | undefined) { let el = elIn; while (el && el !== document.body) { if (getComputedStyle(el).display === 'none') return false; el = el.parentElement; } return true; } static richTextMenuPlugin(props: FormattedTextBoxProps) { return new Plugin({ view: action((newView: EditorView) => { props?.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView); return new RichTextMenuPlugin({ editorProps: props }); }), }); } _didScroll = false; _scrollStopper: undefined | (() => void); scrollToSelection = () => { if (this.EditorView && this._ref.current) { const editorView = this.EditorView; const docPos = editorView.coordsAtPos(editorView.state.selection.to); 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 * this.ScreenToLocalBoxXf().Scale; if (this._focusSpeed !== undefined) { setTimeout(() => { scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed || 0, scrollRef, scrollPos, 'ease', this._scrollStopper)); }); } else { scrollRef.scrollTo({ top: scrollPos }); } this._didScroll = true; } } return true; }; // 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) { this.EditorView?.destroy(); const edState = () => { try { return rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config); } catch { return EditorState.create(config); } }; this._editorView = new EditorView(this.ProseRef, { state: edState(), handleScrollToSelection: this.scrollToSelection, dispatchTransaction: this.dispatchTransaction, nodeViews: FormattedTextBox._nodeViews(this), clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); // bcz: major hack! a patch to prosemirror broke scrolling to selection when the selection is not a dom selection // this replaces prosemirror's scrollToSelection function with Dash's (this.EditorView as unknown as { scrollToSelection: unknown }).scrollToSelection = this.scrollToSelection; const { state } = this._editorView; if (!rtfField) { const layoutProto = DocCast(this.layoutDoc.proto); const dataDoc = layoutProto && Doc.IsDelegateField(layoutProto, this.fieldKey) ? layoutProto : this.dataDoc; const startupText = Field.toString(dataDoc[fieldKey] as FieldType); const textAlign = StrCast(this.dataDoc[this.fieldKey + '_align'], StrCast(Doc.UserDoc().textAlign)) || 'left'; if (textAlign !== 'left') { selectAll(this._editorView.state, tr => { this.EditorView?.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); }); } if (startupText) { this.EditorView?.dispatch(this.EditorView.state.tr.insertText(startupText)); } this.tryUpdateDoc(true); } this._editorView.TextView = this; } const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.rootDoc, DocumentView.SelectOnLoad) && (!DocumentView.LightboxDoc() || DocumentView.LightboxContains(this.DocumentView?.())); const selLoadChar = FormattedTextBox.SelectOnLoadChar; if (selectOnLoad) { DocumentView.SetSelectOnLoad(undefined); FormattedTextBox.SelectOnLoadChar = ''; } if (this.EditorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const { $from } = this.EditorView.state.selection; const mark = schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() }); const curMarks = this.EditorView.state.storedMarks ?? $from?.marksAcross(this.EditorView.state.selection.$head) ?? []; const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark]; if (selLoadChar === 'Enter') { splitBlock(this.EditorView.state, (tx3: Transaction) => this.EditorView?.dispatch(tx3.setStoredMarks(storedMarks))); } else if (selLoadChar) { this.EditorView.dispatch(this.EditorView.state.tr.replaceSelectionWith(this.EditorView.state.schema.text(selLoadChar, storedMarks))); // prettier-ignore } else this.EditorView.dispatch(this.EditorView.state.tr.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 } if (selectOnLoad) { this._liveTextUndo = FormattedTextBox.LiveTextUndo; FormattedTextBox.LiveTextUndo = undefined; this.EditorView!.focus(); } if (this._props.isContentActive()) this.prepareForTyping(); if (this.EditorView && FormattedTextBox.PasteOnLoad) { this.doPaste(this.EditorView, FormattedTextBox.PasteOnLoad.clipboardData); FormattedTextBox.PasteOnLoad = undefined; } if (this._props.autoFocus) setTimeout(() => this.EditorView!.focus()); // not sure why setTimeout is needed but editing dashFieldView's doesn't work without it. } // 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) { 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() })]; 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() { if (this.recordingDictation) { this.recordingDictation = !this.recordingDictation; } removeStyleSheet(this._userStyleSheetElement); Object.values(this._disposers).forEach(disposer => disposer?.()); this.endUndoTypingBatch(); this._liveTextUndo?.end(); this.unhighlightSearchTerms(); this.EditorView?.destroy(); RichTextMenu.Instance?.TextView === this && RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined); FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = 'none'); } onPointerDown = (e: React.PointerEvent): void => { 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. const target = e.target as HTMLElement; if (target.tagName === 'AUDIOTAG') { e.preventDefault(); e.stopPropagation(); 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; const func = () => { const docView = DocumentView.getDocumentView(audiodoc); if (!docView) { this._props.addDocTab(audiodoc, OpenWhere.addBottom); setTimeout(func); } else docView.ComponentView?.playFrom?.(timecode, Cast(anchor.timecodeToHide, 'number', null)); // bcz: would be nice to find the next audio tag in the doc and play until that }; func(); } }); } if (this.recordingDictation && !e.ctrlKey && e.button === 0) { this.breakupDictation(); } FormattedTextBoxComment.textBox = this; if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // 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 HTMLElement).focus(); } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } }; onSelectEnd = () => { GPTPopup.Instance.setSidebarFieldKey(this.sidebarKey); GPTPopup.Instance.addDoc = this.sidebarAddDocument; 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 clickTarget: HTMLElement | Element | null = e.target as HTMLElement; // hrefs are stored on the dataset of the
node that wraps the hyerlink 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; if (dataset?.targethrefs && !dataset.targethrefs.startsWith('/doc')) window .open( dataset?.targethrefs ?.trim() .split(' ') .filter(h => h) .lastElement(), '_blank' ) ?.focus(); else FormattedTextBoxComment.update(this, this.EditorView!, undefined, dataset?.targethrefs, dataset?.linkdoc, dataset?.nopreview === 'true'); } }; @action onDoubleClick = (e: React.MouseEvent): void => { FormattedTextBoxComment.textBox = this; if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) { if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // stop propagation if not in sidebar e.stopPropagation(); // if the text box is selected, then it consumes all click events } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); } FormattedTextBoxComment.Hide(); if (e.buttons === 1 && this._props.rootSelected?.() && !e.altKey) { e.stopPropagation(); } }; 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 HTMLElement).focus(), 200); }; @action onFocused = (e: React.FocusEvent): void => { // applyDevTools.applyDevTools(this.EditorView); e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { if (!this._props.isContentActive()) return; 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 = 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) { const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div 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 editorView.focus(); // editorView.dispatch(editorView.state.tr.setSelection(TextSelection.create(editorView.state.doc, editorView.state.doc.content.size))); } } 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 (editorView && this._props.rootSelected?.()) { // if text box is selected, then it consumes all click events e.stopPropagation(); this.hitBulletTargets(e.clientX, e.clientY, !editorView.state.selection.empty || this._forceUncollapse, false, e.shiftKey); } 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) { this._forceUncollapse = false; clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); const clickPos = this.EditorView!.posAtCoords({ left: x, top: y }); const clickPosVal = clickPos?.pos || 1; let olistPos = clickPosVal; if (clickPos && olistPos && this._props.rootSelected?.()) { const clickNode = this.EditorView?.state.doc.resolve(olistPos).node(); const nodeBef = this.EditorView?.state.doc.resolve(Math.max(0, olistPos - 1)).node(); olistPos = nodeBef?.type === this.EditorView?.state.schema.nodes.ordered_list ? olistPos - 1 : olistPos; 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.depth) { olistNode = $olistPos.parent; $olistPos = this.EditorView?.state.doc.resolve($olistPos.start($olistPos.depth - 1)); } } const maxSize = this.EditorView?.state.doc.content.size ?? 0; const listPos = this.EditorView?.state.doc.resolve(Math.min(maxSize, clickPosVal === olistPos ? clickPosVal + 1 : clickPosVal)); const listNode = listPos?.node(); if (olistNode && olistNode.type === this.EditorView?.state.schema.nodes.ordered_list && listNode) { if (!highlightOnly) { if (selectOrderedList) { this.EditorView.dispatch(this.EditorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); } else { const nodePos = clickPosVal - (olistPos === clickPosVal ? 0 : 1); if (this.EditorView.state.doc.nodeAt(nodePos)) { const tr = this.EditorView.state.tr.setNodeMarkup(nodePos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); this.EditorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, nodePos))); } } } addStyleSheetRule(FormattedTextBox._bulletStyleSheet, olistNode.attrs.mapStyle + olistNode.attrs.bulletStyle + ':hover:before', { background: 'gray' }); } } } startUndoTypingBatch() { !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.Document.title)); } public endUndoTypingBatch() { this._undoTyping?.end(); this._undoTyping = undefined; } /** * When a text box loses focus, it might be because a text button was clicked (eg, bold, italics) or color picker. * In these cases, force focus back onto the text box. * @param target * @returns true if focus was kept on the text box, false otherwise */ public static tryKeepingFocus(target: Element | null, refocusFunc?: () => void) { for (let newFocusEle = target instanceof HTMLElement ? target : null; newFocusEle; newFocusEle = newFocusEle?.parentElement) { // bcz: HACK!! test if parent of new focused element is a UI button (should be more specific than testing className) if (['fonticonbox', 'antimodeMenu-cont', 'popup-container'].includes(newFocusEle?.className ?? '')) { refocusFunc?.(); // keep focus on text box return true; } } return false; } @action onBlur = (e: React.FocusEvent) => { FormattedTextBox.tryKeepingFocus(e.relatedTarget, () => this.EditorView?.focus()); 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(); if (!(this.EditorView?.state.selection instanceof NodeSelection) && typeof this.dataDoc[this.fieldKey] !== 'number') { this.autoLink(); if (this.EditorView?.state.tr) { const tr = stordMarks?.reduce((tr2, m) => { tr2.addStoredMark(m); return tr2; }, this.EditorView.state.tr); tr && this.EditorView.dispatch(tr); } } } if (RichTextMenu.Instance?.view === this.EditorView && !(this._props.isContentActive() || this._props.rootSelected?.())) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined); } // this is the markdown for @ document publishing to Doc.myPublishedDocs 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; 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)); } this.endUndoTypingBatch(); this._liveTextUndo?.end(); // 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') { this._props.setTitleFocus?.(); StopEvent(e); return; } const { state } = _editorView; if (!state.selection.empty && e.key === '%') { this._enteringStyle = true; StopEvent(e); return; } if (this._enteringStyle && 'tix!'.includes(e.key)) { const node = state.selection.$from.nodeAfter; const start = state.selection.from; const end = state.selection.to; if (node) { StopEvent(e); _editorView.dispatch( state.tr .removeMark(start, end, schema.marks.user_mark) // .addMark(start, end, schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })) ); return; } } if (state.selection.empty || !this._enteringStyle) { this._enteringStyle = false; } 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 !== ClientUtils.CurrentUserEmail()) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) { e.preventDefault(); } } switch (e.key) { case 'Escape': this.EditorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); (document.activeElement as HTMLElement).blur?.(); 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 'Space': case 'Backspace': break; default: if ([AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document))) { const mark = state.selection.$to.marks().find(m => m.type === schema.marks.user_mark); _editorView.dispatch( state.tr .removeStoredMark(schema.marks.user_mark) // .addStoredMark(mark ?? schema.marks.user_mark.create({ userid: ClientUtils.CurrentUserEmail() })) ); } break; } e.stopPropagation(); this.startUndoTypingBatch(); }; ondrop = (e: React.DragEvent) => { this.EditorView?.dispatch(updateBullets(this.EditorView.state.tr, this.EditorView.state.schema)); e.stopPropagation(); // drag n drop of text within text note will generate a new note if not caughst, as will dragging in from outside of Dash. }; onScroll = (e: React.UIEvent) => { if (!LinkInfo.Instance?.LinkInfo && this._scrollRef) { this._ignoreScroll = true; this.layoutDoc._layout_scrollTop = this._scrollRef.scrollTop; this._ignoreScroll = false; e.stopPropagation(); e.preventDefault(); } }; tryUpdateScrollHeight = () => { const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yMargin || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (this.EditorView && children && !SnappingManager.IsDragging) { const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), 0) ?? 0; const toNum = (val: string) => Number(val.replace('px', '')); const toHgt = (node: Element): number => { const { height, marginTop, marginBottom } = getComputedStyle(node); const childHeight = height === 'auto' ? getChildrenHeights(Array.from(node.children)) : toNum(height); return childHeight + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); }; const proseHeight = !this.ProseRef ? 0 : getChildrenHeights(children) + margins; 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.layoutDoc['_' + this.fieldKey + '_scrollHeight'] = scrollHeight; }; if (this.Document === this.layoutDoc || this.layoutDoc.rootDocument) { setScrollHeight(); } else { setTimeout(setScrollHeight, 10); // if we have a template that hasn't been resolved yet, we can't set the height or we'd be setting it on the unresolved template. So set a timeout and hope its arrived... } } } }; fitContentsToBox = () => BoolCast(this.Document._freeform_fitContentsToBox); nativeScaling = () => this._props.NativeDimScaling?.() || 1; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.sidebarKey) => { if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; 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.layoutDoc['_' + this.sidebarKey + '_height'] = height; }; sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth(); sidebarScreenToLocal = () => this._props .ScreenToLocalTransform() .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / this.nativeScaling(), 0) .scale(1 / this.nativeScaling()); @computed get audioHandle() { return !this.recordingDictation ? null : (
setupMoveUpEvents( this, e, returnFalse, emptyFunction, action(() => { this.recordingDictation = !this.recordingDictation; }) ) }>
); } private _sideBtnWidth = 35; /** * How much the content of the view is being scaled based on its nesting and its fit-to-width settings */ @computed get viewScaling() { return this.ScreenToLocalBoxXf().Scale ; } // prettier-ignore /** * The maximum size a UI widget can be scaled so that it won't be bigger in screen pixels than its normal 35 pixel size. */ @computed get maxWidgetSize() { return Math.min(this._sideBtnWidth, 0.2 * this._props.PanelWidth())*this.viewScaling; } // prettier-ignore /** * How much to reactively scale a UI element so that it is as big as it can be (up to its normal 35pixel size) without being too big for the Doc content */ @computed get uiBtnScaling() { return this.maxWidgetSize / this._sideBtnWidth; } // prettier-ignore @computed get sidebarHandle() { 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' : '')) as string); return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : (
); } @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 ? ( ) : (
setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
); }; return (
{renderComponent(StrCast(this.layoutDoc[this.sidebarKey + '_type_collection']))}
); } cycleAlternateText = (skipHover?: boolean) => { this.layoutDoc._layout_enableAltContentUI = true; const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; this.layoutDoc[`_${this._props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' && !skipHover ? 'alternate:hover' : undefined; }; @computed get overlayAlternateIcon() { const usePath = this.layoutDoc[`_${this._props.fieldKey}_usePath`]; return ( toggle (%/) between primary and alternate and show alternate on hover }>
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', color: usePath === undefined ? 'black' : 'white', }}>
); } get fieldKey() { return this._fieldKey; } @computed get _fieldKey() { const usePath = StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]); return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._props.isHovering?.() || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : ''); } onPassiveWheel = (e: WheelEvent) => { if (e.clientX > this.ProseRef!.getBoundingClientRect().right) { 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.nativeScaling(); const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., const height = Number(styleFromLayout.height?.replace('px', '')); // prevent default if selected || child is active but this doc isn't scrollable if ( !isNaN(height) && (this._scrollRef?.scrollHeight ?? 0) <= Math.ceil((height || this._props.PanelHeight()) / scale) && // (this._props.rootSelected?.() || this.isAnyChildContentActive()) ) { e.preventDefault(); } e.stopPropagation(); } }; render() { TraceMobx(); const scale = this.nativeScaling(); const rounded = StrCast(this.layoutDoc._layout_borderRounding) === '100%' ? '-rounded' : ''; setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide); const paddingX = Math.max(NumCast(this.layoutDoc._xMargin), 0, this._props.xMargin ?? 0, this._props.screenXPadding?.(this._props.DocumentView?.()) ?? 0); const paddingY = Math.max(NumCast(this.layoutDoc._yMargin), 0, this._props.yMargin ?? 0); // prettier-ignore const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., return this.isLabel ? ( ) : styleFromLayout?.height === '0px' ? null : (
this.fixWheelEvents(r, this._props.isContentActive, this.onPassiveWheel)} style={{ ...(this._props.dontScale ? {} : { transform: `scale(${scale})`, width: `${100 / scale}%`, height: `${100 / scale}%`, }), transition: 'inherit', // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, color: this.fontColor, fontSize: this.fontSize, fontFamily: this.fontFamily, fontWeight: this.fontWeight, fontStyle: this.fontStyle, textDecoration: this.fontDecoration, ...styleFromLayout, }}>
this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)} onBlur={this.onBlur} onPointerUp={this.onPointerUp} onPointerDown={this.onPointerDown} onDoubleClick={this.onDoubleClick}>
{ this._scrollRef = r; }} style={{ width: this.noSidebar ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, overflow: this.layoutDoc._createDocOnCR || this.layoutDoc._layout_hideScroll ? 'hidden' : this.layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}>
{this.noSidebar || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} {this.noSidebar || this.Document._layout_noSidebar || this.Document._createDocOnCR || this.layoutDoc._chromeHidden || this.Document.quiz ? null : this.sidebarHandle} {this.audioHandle} {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
); } } 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', }, });