import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { action, computed, IReactionDisposer, makeObservable, observable, ObservableSet, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import { baseKeymap, selectAll } 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, TextSelection, Transaction } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import * as React from 'react';
import { BsMarkdownFill } from 'react-icons/bs';
import { DateField } from '../../../../fields/DateField';
import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DocCss, DocData, ForceServerWrite, UpdatingFromServer } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
import { InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { PrefetchProxy } from '../../../../fields/Proxy';
import { RichTextField } from '../../../../fields/RichTextField';
import { ComputedField } from '../../../../fields/ScriptField';
import { BoolCast, Cast, DateCast, DocCast, FieldValue, NumCast, RTFCast, ScriptCast, StrCast } from '../../../../fields/Types';
import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util';
import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, DivWidth, emptyFunction, numberRange, returnFalse, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils';
import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT';
import { DocServer } from '../../../DocServer';
import { Docs, DocUtils } from '../../../documents/Documents';
import { CollectionViewType } from '../../../documents/DocumentTypes';
import { DictationManager } from '../../../util/DictationManager';
import { DocumentManager } from '../../../util/DocumentManager';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { MakeTemplate } from '../../../util/DropConverter';
import { LinkManager } from '../../../util/LinkManager';
import { RTFMarkup } from '../../../util/RTFMarkup';
import { SelectionManager } from '../../../util/SelectionManager';
import { SnappingManager } from '../../../util/SnappingManager';
import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
import { CollectionStackingView } from '../../collections/CollectionStackingView';
import { CollectionTreeView } from '../../collections/CollectionTreeView';
import { ContextMenu } from '../../ContextMenu';
import { ContextMenuProps } from '../../ContextMenuItem';
import { ViewBoxAnnotatableComponent, ViewBoxInterface } from '../../DocComponent';
import { Colors } from '../../global/globalEnums';
import { LightboxView } from '../../LightboxView';
import { AnchorMenu } from '../../pdf/AnchorMenu';
import { GPTPopup } from '../../pdf/GPTPopup/GPTPopup';
import { SidebarAnnos } from '../../SidebarAnnos';
import { StyleProp } from '../../StyleProvider';
import { media_state } from '../AudioBox';
import { DocumentView, DocumentViewInternal, OpenWhere } from '../DocumentView';
import { FieldView, FieldViewProps, FocusViewOptions } from '../FieldView';
import { LinkInfo } from '../LinkDocPreview';
import { PinProps, PresBox } from '../trails';
import { DashDocCommentView } from './DashDocCommentView';
import { DashDocView } from './DashDocView';
import { DashFieldView } from './DashFieldView';
import { EquationView } from './EquationView';
import { FootnoteView } from './FootnoteView';
import './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 { SummaryView } from './SummaryView';
// import * as applyDevTools from 'prosemirror-dev-tools';

interface FormattedTextBoxProps extends FieldViewProps {
    onBlur?: () => void; // callback when text loses focus
    autoFocus?: boolean; // whether text should get input focus  when created
}
@observer
export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextBoxProps>() implements ViewBoxInterface {
    public static LayoutString(fieldStr: string) {
        return FieldView.LayoutString(FormattedTextBox, fieldStr);
    }
    public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
    public static Instance: FormattedTextBox;
    public static LiveTextUndo: UndoManager.Batch | undefined;
    static _globalHighlightsCache: string = '';
    static _globalHighlights = new ObservableSet<string>(['Audio Tags', 'Text from Others', 'Todo Items', 'Important Items', 'Disagree Items', 'Ignore Items']);
    static _highlightStyleSheet: any = addStyleSheet();
    static _bulletStyleSheet: any = addStyleSheet();
    static _userStyleSheet: any = addStyleSheet();
    static _hadSelection: boolean = false;
    private _selectionHTML: string | undefined;
    private _sidebarRef = React.createRef<SidebarAnnos>();
    private _sidebarTagRef = React.createRef<React.Component>();
    private _ref: React.RefObject<HTMLDivElement> = React.createRef();
    private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef();
    private _editorView: Opt<EditorView>;
    public _applyingChange: string = '';
    private _inDrop = false;
    private _finishingLink = false;
    private _searchIndex = 0;
    private _lastTimedMark: Mark | undefined = undefined;
    private _cachedLinks: Doc[] = [];
    private _undoTyping?: UndoManager.Batch;
    private _disposers: { [name: string]: IReactionDisposer } = {};
    private _dropDisposer?: DragManager.DragDropDisposer;
    private _recordingStart: number = 0;
    private _ignoreScroll = false;
    private _focusSpeed: Opt<number>;
    private _keymap: any = undefined;
    private _rules: RichTextRules | undefined;
    private _forceUncollapse = true; // if the cursor doesn't move between clicks, then the selection will disappear for some reason.  This flags the 2nd click as happening on a selection which allows bullet points to toggle
    private _forceDownNode: Node | undefined;
    private _downX = 0;
    private _downY = 0;
    private _downTime = 0;
    private _break = true;
    public ProseRef?: HTMLDivElement;
    public get EditorView() {
        return this._editorView;
    }
    public get SidebarKey() {
        return this.fieldKey + '_sidebar';
    }
    @computed get allSidebarDocs() {
        return DocListCast(this.dataDoc[this.SidebarKey]);
    }

    @computed get noSidebar() {
        return this.DocumentView?.()._props.hideDecorationTitle || this._props.noSidebar || this.Document._layout_noSidebar;
    }
    @computed get layout_sidebarWidthPercent() {
        return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%');
    }
    @computed get sidebarColor() {
        return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4'));
    }
    @computed get layout_autoHeight() {
        return (this._props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this._props.ignoreAutoHeight;
    }
    @computed get textHeight() {
        return NumCast(this.dataDoc[this.fieldKey + '_height']);
    }
    @computed get scrollHeight() {
        return NumCast(this.dataDoc[this.fieldKey + '_scrollHeight']);
    }
    @computed get sidebarHeight() {
        return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.SidebarKey + '_height']);
    }
    @computed get titleHeight() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) || 0;
    }
    @computed get layout_autoHeightMargins() {
        return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins);
    }
    @computed get _recordingDictation() {
        return this.dataDoc?.mediaState === media_state.Recording;
    }
    set _recordingDictation(value) {
        !this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? media_state.Recording : undefined);
    }
    @computed get config() {
        this._keymap = buildKeymap(schema, this._props);
        this._rules = new RichTextRules(this.Document, this);
        return {
            schema,
            plugins: [
                inputRules(this._rules.inpRules),
                this.richTextMenuPlugin(),
                history(),
                keymap(this._keymap),
                keymap(baseKeymap),
                new Plugin({ props: { attributes: { class: 'ProseMirror-example-setup-style' } } }),
                new Plugin({
                    view(editorView) {
                        return new FormattedTextBoxComment(editorView);
                    },
                }),
            ],
        };
    }

    // State for GPT
    @observable
    private gptRes: string = '';

    public static PasteOnLoad: ClipboardEvent | undefined;
    private static SelectOnLoad: Doc | undefined;
    public static SetSelectOnLoad(doc: Doc) {
        FormattedTextBox.SelectOnLoad = doc;
    }
    public static DontSelectInitialText = false; // whether initial text should be selected or not
    public static SelectOnLoadChar = '';
    public static IsFragment(html: string) {
        return html.indexOf('data-pm-slice') !== -1;
    }
    public static GetHref(html: string): string {
        const parser = new DOMParser();
        const parsedHtml = parser.parseFromString(html, 'text/html');
        if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 && (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
            return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
        }
        return '';
    }
    public static GetDocFromUrl(url: string) {
        return url.startsWith(document.location.origin) ? new URL(url).pathname.split('doc/').lastElement() : ''; // docId
    }

    constructor(props: FormattedTextBoxProps) {
        super(props);
        makeObservable(this);
        FormattedTextBox.Instance = this;
        this._recordingStart = Date.now();
    }

    // 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 = linkDoc?.link_anchor_1 as Doc;
        const a2 = linkDoc?.link_anchor_2 as Doc;
        if (state && a1 && a2 && this._editorView) {
            this.removeDocument(a1);
            this.removeDocument(a2);
            var allFoundLinkAnchors: any[] = [];
            state.doc.nodesBetween(0, state.doc.nodeSize - 2, (node: any, pos: number, parent: any) => {
                const foundLinkAnchors = findLinkMark(node.marks)?.attrs.allAnchors.filter((a: any) => a.anchorId === a1[Id] || a.anchorId === a2[Id]) || [];
                allFoundLinkAnchors = foundLinkAnchors.length ? foundLinkAnchors : allFoundLinkAnchors;
                return true;
            });
            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) => {
        const rootDoc = Doc.isTemplateDoc(this._props.docViewPath().lastElement()?.Document) ? this.Document : DocCast(this.Document.rootDocument, this.Document);
        if (!pinProps && this._editorView?.state.selection.empty) return rootDoc;
        const anchor = Docs.Create.ConfigDocument({ title: StrCast(rootDoc.title), annotationOn: rootDoc });
        this.addDocument(anchor);
        this._finishingLink = true;
        this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation);
        this._finishingLink = false;
        PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.Document);
        return anchor;
    };

    @action
    setupAnchorMenu = () => {
        AnchorMenu.Instance.Status = 'marquee';

        AnchorMenu.Instance.OnClick = (e: PointerEvent) => {
            !this.layoutDoc.layout_showSidebar && this.toggleSidebar();
            setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
        };
        AnchorMenu.Instance.OnAudio = (e: PointerEvent) => {
            !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: any;
                    const targetData = target[DocData];
                    targetData.mediaState = media_state.Recording;
                    DocumentViewInternal.recordAudioAnnotation(targetData, Doc.LayoutFieldKey(target), stop => (stopFunc = stop));
                    const reactionDisposer = reaction(
                        () => target.mediaState,
                        dictation => {
                            if (!dictation) {
                                stopFunc();
                                reactionDisposer();
                            }
                        }
                    );
                    target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`);
                }
            });
        };
        AnchorMenu.Instance.Highlight = undoable((color: string) => {
            this._editorView?.state && RichTextMenu.Instance.setHighlight(color);
            return undefined;
        }, '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);
                FormattedTextBox.SetSelectOnLoad(target);
                return target;
            };

            DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
        });
        const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
        this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
        let ele: Opt<HTMLDivElement> = undefined;
        try {
            const contents = window.getSelection()?.getRangeAt(0).cloneContents();
            if (contents) {
                ele = document.createElement('div');
                ele.append(contents);
            }
            this._selectionHTML = ele?.innerHTML;
        } catch (e) {}
    };

    leafText = (node: Node) => {
        if (node.type === this._editorView?.state.schema.nodes.dashField) {
            const refDoc = !node.attrs.docId ? DocCast(this.Document.rootDocument, this.Document) : (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 Field))
            );
        }
        return '';
    };
    dispatchTransaction = (tx: Transaction) => {
        if (this._editorView && (this._editorView as any).docView) {
            const state = this._editorView.state.apply(tx);
            this._editorView.updateState(state);
            this.tryUpdateDoc(false);
        }
    };

    tryUpdateDoc = (force: boolean) => {
        if (this._editorView && (this._editorView as any).docView) {
            const state = this._editorView.state;
            const dataDoc = this.dataDoc;
            const newText = state.doc.textBetween(0, state.doc.content.size, ' \n', this.leafText);
            const newJson = JSON.stringify(state.toJSON());
            const prevData = Cast(this.layoutDoc[this.fieldKey], RichTextField, null); // the actual text in the text box
            const templateData = this.Document !== this.layoutDoc ? prevData : undefined; // the default text stored in a layout template
            const protoData = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype
            const layoutData = this.layoutDoc.isTemplateDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text inherited from a prototype
            const effectiveAcl = GetEffectiveAcl(dataDoc);

            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: any, 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<string>(Array.from(new Set<string>(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
                if (this._applyingChange !== this.fieldKey && (force || textChange || removeSelection(newJson) !== removeSelection(prevData?.Data))) {
                    this._applyingChange = this.fieldKey;
                    textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now())));
                    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)))) {
                            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 });
                            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 we've deleted all the text in a note driven by a template, then restore the template data
                        dataDoc[this.fieldKey] = undefined;
                        this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(((layoutData !== prevData ? layoutData : undefined) ?? protoData).Data)));
                        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;
        let link;
        LinkManager.Links(this.dataDoc).forEach((l, i) => {
            const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined;
            if (anchor && (anchor.annotationOn as Doc).mediaState === media_state.Recording) {
                linkTime = NumCast(anchor._timecodeToShow /* audioStart */);
                linkAnchor = anchor;
                link = l;
            }
        });
        if (this._editorView && linkTime) {
            const state = this._editorView.state;
            const now = Date.now();
            let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
            if (!this._break && state.selection.to !== state.selection.from) {
                for (let i = state.selection.from; i <= state.selection.to; i++) {
                    const pos = state.doc.resolve(i);
                    const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
                    if (um) {
                        mark = um;
                        break;
                    }
                }
            }

            const path = (this._editorView.state.selection.$from as any).path;
            if (linkAnchor && path[path.length - 3].type !== this._editorView.state.schema.nodes.code_block) {
                const time = linkTime + Date.now() / 1000 - this._recordingStart / 1000;
                this._break = false;
                const from = state.selection.from;
                const value = this._editorView.state.schema.nodes.audiotag.create({ timeCode: time, audioId: linkAnchor[Id] });
                const replaced = this._editorView.state.tr.insert(from - 1, value);
                this._editorView.dispatch(replaced.setSelection(new TextSelection(replaced.doc.resolve(from + 1))));
            }
        }
    };

    autoLink = () => {
        const newAutoLinks = new Set<Doc>();
        const oldAutoLinks = LinkManager.Links(this.Document).filter(
            link => 
                ((!Doc.isTemplateForField(this.Document) &&
                    (!Doc.isTemplateForField(DocCast(link.link_anchor_1)) || !Doc.AreProtosEqual(DocCast(link.link_anchor_1), this.Document)) &&
                    (!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) {
            const f = this._editorView.state.selection.from;

            const t = this._editorView.state.selection.to;
            var tr = this._editorView.state.tr as any;
            const autoAnch = this._editorView.state.schema.marks.autoLinkAnchor;
            tr = tr.removeMark(0, tr.doc.content.size, autoAnch);
            Doc.MyPublishedDocs.filter(term => term.title).forEach(term => (tr = this.hyperlinkTerm(tr, term, newAutoLinks)));
            tr = tr.setSelection(new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t)));
            this._editorView?.dispatch(tr);
        }
        oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.Document).forEach(LinkManager.Instance.deleteLink);
    };

    updateTitle = () => {
        const title = StrCast(this.dataDoc.title, Cast(this.dataDoc.title, RichTextField, null)?.Text);
        if (
            !this._props.dontRegisterView && // (this.Document.isTemplateForField === "text" || !this.Document.isTemplateForField) && // only update the title if the data document's data field is changing
            (title.startsWith('-') || title.startsWith('@')) &&
            this._editorView &&
            !this.dataDoc.title_custom &&
            (Doc.LayoutFieldKey(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 = str.startsWith('@') ? '' : '-';

            const cfield = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc.title));
            if (!(cfield instanceof ComputedField)) {
                this.dataDoc.title = (prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? '...' : '')).trim();
                if (str.startsWith('@') && str.length > 1) {
                    Doc.AddToMyPublished(this.Document);
                }
            }
        }
    };

    // 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, @<name>, or ^@<name>
     * 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 = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => {
        const editorView = this._editorView;
        if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.Document)) {
            const autoLinkTerm = StrCast(target.title).replace(/^@/, '');
            var 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: any, pos: number, parent: any) => {
                        if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) {
                            alink =
                                alink ??
                                (LinkManager.Links(this.Document).find(
                                    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) => {
        if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
            const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
            const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
            const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
            const length = res[0].length;
            let tr = this._editorView.state.tr;
            const 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] && this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
        }
    };

    unhighlightSearchTerms = () => {
        if (window.screen.width < 600) null;
        else if (this._editorView && (this._editorView as any).docView) {
            const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
            const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
            const end = this._editorView.state.doc.nodeSize - 2;
            this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
        }
    };
    adoptAnnotation = (start: number, end: number, mark: Mark) => {
        const view = this._editorView!;
        const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail });
        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) {
            const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
            const effectiveAcl = GetEffectiveAcl(dataDoc);
            const draggedDoc = dragData.droppedDocuments.lastElement();
            let added: Opt<boolean>;
            const dropAction = dragData.dropAction || dragData.userDropAction;
            if ([AclEdit, AclAdmin, AclSelfEdit].includes(effectiveAcl)) {
                // replace text contents when dragging with Alt
                if (de.altKey) {
                    const fieldKey = Doc.LayoutFieldKey(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) ? true : false;
                    } 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 ? true : false; // pos will be null if you don't drop onto an actual text location
                        } catch (e) {
                            console.log('Drop failed', e);
                            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 as any).content.length; i++) {
                const result = this.getNodeEndpoints((context.content as any).content[i], node);
                if (result) {
                    return {
                        from: result.from + offset + (context.type.name === 'doc' ? 0 : 1),
                        to: result.to + offset + (context.type.name === 'doc' ? 0 : 1),
                    };
                }
                offset += (context.content as any).content[i].nodeSize;
            }
            return null;
        } else {
            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,
                foundAt;
            const ep = this.getNodeEndpoints(pm.state.doc, node);
            const regexp = new RegExp(find, 'i');
            if (regexp) {
                var blockOffset = 0;
                for (var i = 0; i < node.childCount; i++) {
                    var textContent = '';
                    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, i) => (ret = ret.concat(this.findInNode(pm, child, find))));
        }
        return ret;
    }

    updateHighlights = (highlights: string[]) => {
        if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return;
        setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join('')));
        clearStyleSheetRules(FormattedTextBox._userStyleSheet);
        if (!highlights.includes('Audio Tags')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, '');
        }
        if (highlights.includes('Text from Others')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-remote', { background: 'yellow' });
        }
        if (highlights.includes('My Text')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace(/\./g, '').replace(/@/g, ''), { background: 'moccasin' });
        }
        if (highlights.includes('Todo Items')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-todo', { outline: 'black solid 1px' });
        }
        if (highlights.includes('Important Items')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-important', { 'font-size': 'larger' });
        }
        if (highlights.includes('Bold Text')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror strong > span', { 'font-size': 'large' }, '');
            addStyleSheetRule(FormattedTextBox._userStyleSheet, '.formattedTextBox-inner .ProseMirror :not(strong > span)', { 'font-size': '0px' }, '');
        }
        if (highlights.includes('Disagree Items')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-disagree', { 'text-decoration': 'line-through' });
        }
        if (highlights.includes('Ignore Items')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UT-ignore', { 'font-size': '1' });
        }
        if (highlights.includes('By Recent Minute')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
            const min = Math.round(Date.now() / 1000 / 60);
            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-min-' + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
        }
        if (highlights.includes('By Recent Hour')) {
            addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-' + Doc.CurrentUserEmail.replace('.', '').replace('@', ''), { opacity: '0.1' });
            const hr = Math.round(Date.now() / 1000 / 60 / 60);
            numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
        }
        this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx.  so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
    };

    @observable _showSidebar = false;
    @computed get SidebarShown() {
        return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false;
    }

    @action
    toggleSidebar = (preview: boolean = false) => {
        const defaultSidebar = 250;
        const prevWidth = 1 - this.sidebarWidth() / DivWidth(this._ref.current!);
        if (preview) this._showSidebar = true;
        else {
            this.layoutDoc[this.SidebarKey + '_freeform_scale_max'] = 1;
            this.layoutDoc._layout_showSidebar =
                (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(defaultSidebar / (NumCast(this.layoutDoc._width) + defaultSidebar)) * 100}%` : '0%') !== '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,
            (e, movement, isClick) => !isClick && batch.end(),
            () => {
                this.toggleSidebar();
                batch.end();
            },
            true
        );
    };
    sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => {
        const localDelta = this._props
            .ScreenToLocalTransform()
            .scale(this._props.NativeDimScaling?.() || 1)
            .transformDirection(delta[0], delta[1]);
        const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_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');
        LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]);
        // const docAnnotations = DocListCast(this._props.dataDoc[this.fieldKey]);
        // this._props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion));
        // AnchorMenu.Instance.fadeOut(true);
        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 => {
        const cm = ContextMenu.Instance;

        const editor = this._editorView!;
        const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
        let target = e.target as any; // hrefs are stored on the database of the <a> node that wraps the hyerlink <span>
        while (target && !target.dataset?.targethrefs) target = target.parentElement;
        if (target && !(e.nativeEvent as any).dash) {
            const hrefs = (target.dataset?.targethrefs as string)
                ?.trim()
                .split(' ')
                .filter(h => h);
            const anchorDoc = Array.from(hrefs ?? [])
                .lastElement()
                .replace(Doc.localServerPath(), '')
                .split('?')[0];
            const deleteMarkups = undoBatch(() => {
                const sel = editor.state.selection;
                editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor));
            });
            e.persist();
            anchorDoc &&
                DocServer.GetRefField(anchorDoc).then(
                    action(anchor => {
                        anchor && SelectionManager.SelectSchemaViewDoc(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 changeItems: ContextMenuProps[] = [];
        changeItems.push({
            description: 'plain',
            event: undoBatch(() => {
                Doc.setNativeView(this.Document);
                this.layoutDoc.layout_autoHeightMargins = undefined;
            }),
            icon: 'eye',
        });
        changeItems.push({
            description: 'metadata',
            event: undoBatch(() => {
                this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout;
                this.Document.layout_fieldKey = 'layout_meta';
                setTimeout(() => (this.layoutDoc._headerHeight = this.layoutDoc._layout_autoHeightMargins = 50), 50);
            }),
            icon: 'eye',
        });
        const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null);
        DocListCast(noteTypesDoc?.data).forEach(note => {
            const icon: IconProp = StrCast(note.icon) as IconProp;
            changeItems.push({
                description: StrCast(note.title),
                event: undoBatch(() => {
                    this.layoutDoc.layout_autoHeightMargins = undefined;
                    Doc.setNativeView(this.Document);
                    DocUtils.makeCustomViewClicked(this.Document, Docs.Create.TreeDocument, StrCast(note.title), note);
                }),
                icon: icon,
            });
        });
        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: (!FormattedTextBox._globalHighlights.has(option) ? 'Highlight ' : 'Unhighlight ') + option,
                event: action(() => {
                    e.stopPropagation();
                    if (!FormattedTextBox._globalHighlights.has(option)) {
                        FormattedTextBox._globalHighlights.add(option);
                    } else {
                        FormattedTextBox._globalHighlights.delete(option);
                    }
                }),
                icon: !FormattedTextBox._globalHighlights.has(option) ? 'highlighter' : 'remove-format',
            })
        );
        const appearance = cm.findByDescription('Appearance...');
        const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : [];

        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',
        });
        !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',
            });

        appearanceItems.push({ description: 'Change Style...', noexpand: true, subitems: changeItems, icon: 'external-link-alt' });

        !Doc.noviceMode &&
            appearanceItems.push({
                description: 'Make Default Layout',
                event: () => {
                    if (!this.layoutDoc.isTemplateDoc) {
                        MakeTemplate(this.Document);
                    }
                    Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.Document);
                    Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.Document);
                },
                icon: 'eye',
            });
        !appearance && appearanceItems.length && cm.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });

        const options = cm.findByDescription('Options...');
        const optionItems = options && 'subitems' in options ? options.subitems : [];
        optionItems.push({ description: `Toggle auto update from template`, event: () => (this.dataDoc[this.fieldKey + '_autoUpdate'] = !this.dataDoc[this.fieldKey + '_autoUpdate']), icon: 'star' });
        optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' });
        optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' });
        this._props.renderDepth &&
            optionItems.push({
                description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns',
                event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR),
                icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars',
            });
        !Doc.noviceMode &&
            optionItems.push({
                description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`,
                event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight),
                icon: this.Document._layout_autoHeight ? 'lock' : 'unlock',
            });
        !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
        const help = cm.findByDescription('Help...');
        const helpItems = help && 'subitems' in help ? help.subitems : [];
        helpItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: <BsMarkdownFill /> });
        !help && cm.addItem({ description: 'Help...', subitems: helpItems, icon: 'eye' });
        this._downX = this._downY = Number.NaN;
    };

    animateRes = (resIndex: number, newText: string) => {
        if (resIndex < newText.length) {
            const marks = this._editorView?.state.storedMarks ?? [];
            this._editorView?.dispatch(this._editorView.state.tr.setStoredMarks(marks).insertText(newText[resIndex]).setStoredMarks(marks));
            setTimeout(() => {
                this.animateRes(resIndex + 1, newText);
            }, 20);
        }
    };

    askGPT = action(async () => {
        try {
            let res = await gptAPICall((this.dataDoc.text as RichTextField)?.Text, GPTCallType.COMPLETION);
            if (!res) {
                console.error('GPT call failed');
                this.animateRes(0, 'Something went wrong.');
            } else {
                this.animateRes(0, res);
            }
        } catch (err) {
            console.error('GPT call failed');
            this.animateRes(0, 'Something went wrong.');
        }
    });

    generateImage = async () => {
        GPTPopup.Instance?.setTextAnchor(this.getAnchor(false));
        GPTPopup.Instance?.setImgTargetDoc(this.Document);
        GPTPopup.Instance.addToCollection = this._props.addDocument;
        GPTPopup.Instance.setImgDesc((this.dataDoc.text as RichTextField)?.Text);
        GPTPopup.Instance.generateImage();
    };

    breakupDictation = () => {
        if (this._editorView && this._recordingDictation) {
            this.stopDictation(true);
            this._break = true;
            const state = this._editorView.state;
            const to = state.selection.to;
            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 = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement();
                if (link) {
                    link[DocData].isDictation = true;
                    const audioanchor = Cast(link.link_anchor_2, Doc, null);
                    const textanchor = Cast(link.link_anchor_1, Doc, null);
                    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[DocData].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.from;
            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 state = this._editorView?.state;
        if (state) {
            let selectedText = '';
            const sel = state.selection;
            const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() });
            let tr = state.tr.addMark(sel.from, sel.to, splitter);
            if (sel.from !== sel.to) {
                const anchor =
                    anchorDoc ??
                    Docs.Create.ConfigDocument({
                        //
                        title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')',
                        annotationOn: this.dataDoc,
                    });
                const href = targetHref ?? Doc.localServerPath(anchor);
                if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor);
                tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => {
                    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(sel.from, sel.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;
    }

    getView = async (doc: Doc, options: FocusViewOptions) => {
        if (DocListCast(this.dataDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) {
            if (!this.SidebarShown) {
                this.toggleSidebar(false);
                options.didMove = true;
            }
            setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc));
        }
        return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv)));
    };
    focus = (textAnchor: Doc, options: FocusViewOptions) => {
        const focusSpeed = options.zoomTime ?? 500;
        const textAnchorId = textAnchor[Id];
        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;
        };

        let start = 0;
        this._didScroll = false; // assume we don't need to scroll.  if we do, this will get set to true in handleScrollToSelextion when we dispatch the setSelection below
        if (this._editorView && textAnchorId) {
            const editor = this._editorView;
            const ret = findAnchorFrag(editor.state.doc.content, editor);

            const content = (ret.frag as any)?.content;
            if ((ret.frag.size || (content?.length && content[0].type === this._editorView.state.schema.nodes.dashDoc) || (content?.length && content[0].type === this._editorView.state.schema.nodes.audiotag)) && ret.start >= 0) {
                !options.instant && (this._focusSpeed = focusSpeed);
                let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
                if (ret.frag.firstChild) {
                    selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
                }
                editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
                const escAnchorId = textAnchorId[0] >= '0' && textAnchorId[0] <= '9' ? `\\3${textAnchorId[0]} ${textAnchorId.substr(1)}` : textAnchorId;
                addStyleSheetRule(FormattedTextBox._highlightStyleSheet, `${escAnchorId}`, { background: 'yellow', transform: 'scale(3)', 'transform-origin': 'left bottom' });
                setTimeout(() => (this._focusSpeed = undefined), this._focusSpeed);
                setTimeout(() => clearStyleSheetRules(FormattedTextBox._highlightStyleSheet), Math.max(this._focusSpeed || 0, 3000));
                return focusSpeed;
            } else {
                return this._props.focus(this.Document, options);
            }
        }
    };

    // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc.
    // Since we also monitor all component height changes, this will update the document's height.
    resetNativeHeight = (scrollHeight: number) => {
        const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight);
        this.dataDoc[this.fieldKey + '_height'] = scrollHeight;
        if (nh) this.layoutDoc._nativeHeight = scrollHeight;
    };

    @computed get contentScaling() {
        return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 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 = LinkManager.Links(this.Document);
        this._disposers.breakupDictation = reaction(() => Doc.RecordingEvent, this.breakupDictation);
        this._disposers.layout_autoHeight = reaction(
            () => ({ autoHeight: this.layout_autoHeight, fontSize: this.fontSize, css: this.Document[DocCss] }),
            (autoHeight, fontSize) => setTimeout(() => autoHeight && this.tryUpdateScrollHeight())
        );
        this._disposers.highlights = reaction(
            () => Array.from(FormattedTextBox._globalHighlights).slice(),
            highlights => this.updateHighlights(highlights),
            { fireImmediately: true }
        );
        this._disposers.width = reaction(
            () => this._props.PanelWidth(),
            width => this.tryUpdateScrollHeight()
        );
        this._disposers.scrollHeight = reaction(
            () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }),
            ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight),
            { fireImmediately: true }
        );
        this._disposers.componentHeights = reaction(
            // set the document height when one of the component heights changes and layout_autoHeight is on
            () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
            ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => {
                const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
                if (
                    (!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && //
                    layout_autoHeight &&
                    newHeight &&
                    newHeight !== this.layoutDoc.height &&
                    !this._props.dontRegisterView
                ) {
                    this._props.setHeight?.(newHeight);
                }
            },
            { fireImmediately: !Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') }
        );
        this._disposers.links = reaction(
            () => LinkManager.Links(this.dataDoc), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks
            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) ?? StrCast(whichData)) };
            },
            incomingValue => {
                if (this._editorView && this._applyingChange !== this.fieldKey) {
                    if (incomingValue?.data) {
                        const updatedState = JSON.parse(incomingValue.data.Data);
                        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 ?? '')));
                    }
                }
            },
            { 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) ? true : false }
        );

        this._disposers.selected = reaction(
            () => this._props.rootSelected?.(),
            action(selected => {
                //selected && setTimeout(() => this.prepareForTyping());
                if (FormattedTextBox._globalHighlights.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 && !selected) {
                    RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
                }
                if (this._editorView && selected) {
                    RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc);
                    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);
        }
        var quickScroll: string | undefined = '';
        this._disposers.scroll = reaction(
            () => NumCast(this.layoutDoc._layout_scrollTop),
            pos => {
                if (!this._ignoreScroll && this._scrollRef.current && !this._props.dontSelectOnLoad) {
                    const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition);
                    const durationMiliStr = viewTrans.match(/([0-9]*)ms/);
                    const durationSecStr = viewTrans.match(/([0-9.]*)s/);
                    const duration = durationMiliStr ? Number(durationMiliStr[1]) : durationSecStr ? Number(durationSecStr[1]) * 1000 : 0;
                    if (duration) {
                        this._scrollStopper = smoothScroll(duration, this._scrollRef.current, Math.abs(pos || 0), 'ease', this._scrollStopper);
                    } else {
                        this._scrollRef.current.scrollTo({ top: pos });
                    }
                }
            },
            { fireImmediately: true }
        );
        quickScroll = undefined;
        this.tryUpdateScrollHeight();
        setTimeout(this.tryUpdateScrollHeight, 250);
    }

    clipboardTextSerializer = (slice: Slice): string => {
        let text = '',
            separated = true;
        const from = 0,
            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 === 'hard_break') {
                    text += '\n';
                }
            },
            0
        );
        return text;
    };

    handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
        const pdfAnchorId = (event as ClipboardEvent).clipboardData?.getData('dash/pdfAnchor');
        return pdfAnchorId && this.addPdfReference(pdfAnchorId) ? true : false;
    };

    addPdfReference = (pdfAnchorId: string) => {
        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(el: Element | null | undefined) {
        while (el && el !== document.body) {
            if (getComputedStyle(el).display === 'none') return false;
            el = el.parentNode as any;
        }
        return true;
    }

    richTextMenuPlugin() {
        const self = this;
        return new Plugin({
            view(newView) {
                runInAction(() => self._props.rootSelected?.() && RichTextMenu.Instance && (RichTextMenu.Instance.view = newView));
                return new RichTextMenuPlugin({ editorProps: this._props });
            },
        });
    }
    _didScroll = false;
    _scrollStopper: undefined | (() => void);
    setupEditor(config: any, fieldKey: string) {
        const curText = Cast(this.dataDoc[this.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.fieldKey]);
        const rtfField = Cast((!curText && this.layoutDoc[this.fieldKey]) || this.dataDoc[fieldKey], RichTextField);
        if (this.ProseRef) {
            const self = this;
            this._editorView?.destroy();
            this._editorView = new EditorView(this.ProseRef, {
                state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
                handleScrollToSelection: editorView => {
                    const docPos = editorView.coordsAtPos(editorView.state.selection.to);
                    const viewRect = self._ref.current!.getBoundingClientRect();
                    const scrollRef = self._scrollRef.current;
                    const topOff = docPos.top < viewRect.top ? docPos.top - viewRect.top : undefined;
                    const botOff = docPos.bottom > viewRect.bottom ? docPos.bottom - viewRect.bottom : undefined;
                    if (((topOff && Math.abs(Math.trunc(topOff)) > 0) || (botOff && Math.abs(Math.trunc(botOff)) > 0)) && scrollRef) {
                        const shift = Math.min(topOff ?? Number.MAX_VALUE, botOff ?? Number.MAX_VALUE);
                        const scrollPos = scrollRef.scrollTop + shift * self.ScreenToLocalBoxXf().Scale;
                        if (this._focusSpeed !== undefined) {
                            scrollPos && (this._scrollStopper = smoothScroll(this._focusSpeed, scrollRef, scrollPos, 'ease', this._scrollStopper));
                        } else {
                            scrollRef.scrollTo({ top: scrollPos });
                        }
                        this._didScroll = true;
                    }
                    return true;
                },
                dispatchTransaction: this.dispatchTransaction,
                nodeViews: {
                    dashComment(node: any, view: any, getPos: any) {
                        return new DashDocCommentView(node, view, getPos);
                    },
                    dashDoc(node: any, view: any, getPos: any) {
                        return new DashDocView(node, view, getPos, self);
                    },
                    dashField(node: any, view: any, getPos: any) {
                        return new DashFieldView(node, view, getPos, self);
                    },
                    equation(node: any, view: any, getPos: any) {
                        return new EquationView(node, view, getPos, self);
                    },
                    summary(node: any, view: any, getPos: any) {
                        return new SummaryView(node, view, getPos);
                    },
                    //ordered_list(node: any, view: any, getPos: any) { return new OrderedListView(); },
                    footnote(node: any, view: any, getPos: any) {
                        return new FootnoteView(node, view, getPos);
                    },
                },
                clipboardTextSerializer: this.clipboardTextSerializer,
                handlePaste: this.handlePaste,
            });
            const { state, dispatch } = this._editorView;
            if (!rtfField) {
                const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc.proto), this.fieldKey) ? DocCast(this.layoutDoc.proto) : this.dataDoc;
                const startupText = Field.toString(dataDoc[fieldKey] as Field);
                if (startupText) {
                    dispatch(state.tr.insertText(startupText));
                }
                const textAlign = StrCast(this.dataDoc.text_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 })));
                    });
                }
            }
            (this._editorView as any).TextView = this;
        }

        const selectOnLoad = Doc.AreProtosEqual(this._props.TemplateDataDocument ?? this.Document, FormattedTextBox.SelectOnLoad) && (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.()));
        const selLoadChar = FormattedTextBox.SelectOnLoadChar;
        if (selectOnLoad) {
            FormattedTextBox.SelectOnLoad = undefined;
            FormattedTextBox.SelectOnLoadChar = '';
        }
        if (this._editorView && selectOnLoad && !this._props.dontRegisterView && !this._props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) {
            this._props.select(false);
            if (selLoadChar) {
                const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined;
                const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
                const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? [];
                const storedMarks = [...curMarks.filter(m => m.type !== mark.type), mark];
                const tr1 = this._editorView.state.tr.setStoredMarks(storedMarks);
                const tr2 = selLoadChar === 'Enter' ? tr1.insert(this._editorView.state.doc.content.size - 1, schema.nodes.paragraph.create()) : tr1.insertText(selLoadChar, this._editorView.state.doc.content.size - 1);
                const tr = tr2.setStoredMarks(storedMarks);

                this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size))));
            } else if (curText && !FormattedTextBox.DontSelectInitialText) {
                selectAll(this._editorView.state, this._editorView?.dispatch);
            }
        }
        if (selectOnLoad) {
            FormattedTextBox.DontSelectInitialText = false;
            this._editorView!.focus();
        }
        if (this._props.isContentActive()) this.prepareForTyping();
        if (this._editorView && FormattedTextBox.PasteOnLoad) {
            const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor');
            FormattedTextBox.PasteOnLoad = undefined;
            pdfAnchorId && this.addPdfReference(pdfAnchorId);
        }
        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) return;
        const docDefaultMarks = [
            ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
            ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []),
            ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []),
            ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily) })] : []),
            ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize) })] : []),
            ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []),
            ...[schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })],
        ];
        this._editorView?.dispatch(this._editorView?.state.tr.setStoredMarks(docDefaultMarks));
    };

    componentWillUnmount() {
        if (this._recordingDictation) {
            this._recordingDictation = !this._recordingDictation;
        }
        Object.values(this._disposers).forEach(disposer => disposer?.());
        this.endUndoTypingBatch();
        FormattedTextBox.LiveTextUndo?.end();
        FormattedTextBox.LiveTextUndo = undefined;
        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 ((e.nativeEvent as any).handledByInnerReactInstance) {
            return; //e.stopPropagation();
        } else (e.nativeEvent as any).handledByInnerReactInstance = true;

        if (this.Document.forceActive) e.stopPropagation();
        this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights.  so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view.
        if ((e.target as any).tagName === 'AUDIOTAG') {
            e.preventDefault();
            e.stopPropagation();
            const timecode = Number((e.target as any)?.dataset?.timecode);
            DocServer.GetRefField((e.target as any)?.dataset?.audioid || 0).then(anchor => {
                if (anchor instanceof Doc) {
                    // const timecode = NumCast(anchor.timecodeToShow, 0);
                    const audiodoc = anchor.annotationOn as Doc;
                    const func = () => {
                        const docView = DocumentManager.Instance.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();
        }
        this._downX = e.clientX;
        this._downY = e.clientY;
        this._downTime = Date.now();
        FormattedTextBoxComment.textBox = this;
        if (e.button === 0 && this._props.rootSelected?.() && !e.altKey && !e.ctrlKey && !e.metaKey) {
            if (e.clientX < this.ProseRef!.getBoundingClientRect().right) {
                // stop propagation if not in sidebar, otherwise nested boxes will lose focus to outer boxes.
                e.stopPropagation(); // if the text box's content is active, then it consumes all down events
                document.addEventListener('pointerup', this.onSelectEnd);
                (this.ProseRef?.children?.[0] as any).focus();
            }
        }
        if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
            e.preventDefault();
        }
    };
    onSelectEnd = (e: PointerEvent) => {
        document.removeEventListener('pointerup', this.onSelectEnd);
    };
    onPointerUp = (e: React.PointerEvent): void => {
        const editor = this._editorView!;
        const state = editor?.state;
        if (!state || !editor || !this.ProseRef?.children[0].className.includes('-focused')) return;
        if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu();
        else if (this._props.isContentActive() && !e.button) {
            const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY });
            let xpos = pcords?.pos || 0;
            while (xpos > 0 && !state.doc.resolve(xpos).node()?.isTextblock) {
                xpos = xpos - 1;
            }
            let node: any;
            try {
                node = state.doc.nodeAt(xpos);
            } catch (e) {}
            if (node?.type !== schema.nodes.dashFieldView) {
                editor.dispatch(state.tr.setSelection(TextSelection.near(state.doc.resolve(xpos))));
                let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span>
                while (target && !target.dataset?.targethrefs) target = target.parentElement;
                FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true');
            } else {
                try {
                    editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos))));
                } catch (e) {
                    editor.dispatch(state.tr.setSelection(new NodeSelection(state.doc.resolve(xpos - 1))));
                }
            }
        }
    };
    @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 any).focus(), 200);
    };
    @action
    onFocused = (e: React.FocusEvent): void => {
        //applyDevTools.applyDevTools(this._editorView);
        this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this._props, this.layoutDoc);
        e.stopPropagation();
    };

    onClick = (e: React.MouseEvent): void => {
        if (!this._props.isContentActive()) return;
        if ((e.nativeEvent as any).handledByInnerReactInstance) {
            e.stopPropagation();
            return;
        }
        if (Math.abs(e.clientX - this._downX) > 4 || Math.abs(e.clientY - this._downY) > 4) {
            this._forceDownNode = undefined;
            return;
        }
        if (!this._forceUncollapse || (this._editorView!.root as any).getSelection().isCollapsed) {
            // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment.  Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
            const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
            const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
            if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
                this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
                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
                    this._editorView?.focus();
                    this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
                }
            } else if (node && [this._editorView!.state.schema.nodes.ordered_list, this._editorView!.state.schema.nodes.listItem].includes(node.type) && node !== (this._editorView!.state.selection as NodeSelection)?.node && pcords) {
                this._editorView!.dispatch(this._editorView!.state.tr.setSelection(NodeSelection.create(this._editorView!.state.doc, pcords.pos)));
            }
        }
        if (this._props.rootSelected?.()) {
            // if text box is selected, then it consumes all click events
            (e.nativeEvent as any).handledByInnerReactInstance = true;
            this.hitBulletTargets(e.clientX, e.clientY, !this._editorView?.state.selection.empty || this._forceUncollapse, false, this._forceDownNode, e.shiftKey);
        }
        this._forceUncollapse = !(this._editorView!.root as any).getSelection().isCollapsed;
        this._forceDownNode = (this._editorView!.state.selection as NodeSelection)?.node;
    };
    // this hackiness handles clicking on the list item bullets to do expand/collapse.  the bullets are ::before pseudo elements so there's no real way to hit test against them.
    hitBulletTargets(x: number, y: number, collapse: boolean, highlightOnly: boolean, downNode: Node | undefined = undefined, selectOrderedList: boolean = false) {
        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 as any).path.length > 3) {
                    olistNode = $olistPos.parent;
                    $olistPos = this._editorView?.state.doc.resolve(($olistPos as any).path[($olistPos as any).path.length - 4]);
                }
            }
            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;
    }

    @action
    onBlur = (e: any) => {
        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)) {
                this.autoLink();
                if (this._editorView?.state.tr) {
                    const tr = stordMarks?.reduce((tr, m) => {
                        tr.addStoredMark(m);
                        return tr;
                    }, this._editorView.state.tr);
                    tr && this._editorView.dispatch(tr);
                }
            }
        }
        if (RichTextMenu.Instance?.view === this._editorView && !this._props.rootSelected?.()) {
            RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined, undefined);
        }
        FormattedTextBox._hadSelection = window.getSelection()?.toString() !== '';
        this.endUndoTypingBatch();

        FormattedTextBox.LiveTextUndo?.end();
        FormattedTextBox.LiveTextUndo = undefined;

        const state = this._editorView!.state;
        if (StrCast(this.Document.title).startsWith('@') && !this.dataDoc.title_custom) {
            UndoManager.RunInBatch(() => {
                this.dataDoc.title_custom = true;
                this.dataDoc.layout_showTitle = 'title';
                const tr = this._editorView!.state.tr;
                this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.Document.title).length + 2))).deleteSelection());
            }, 'titler');
        }
        // 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) => {
        if ((e.altKey || e.ctrlKey) && e.key === 't') {
            e.preventDefault();
            e.stopPropagation();
            this._props.setTitleFocus?.();
            return;
        }
        const state = this._editorView!.state;
        if (!state.selection.empty && e.key === '%') {
            this._rules!.EnteringStyle = true;
            e.preventDefault();
            e.stopPropagation();
            return;
        }

        if (state.selection.empty || !this._rules!.EnteringStyle) {
            this._rules!.EnteringStyle = false;
        }
        let stopPropagation = true;
        for (var i = state.selection.from; i <= state.selection.to; i++) {
            const node = state.doc.resolve(i);
            if (state.doc.content.size - 1 > i && node?.marks?.().some(mark => mark.type === schema.marks.user_mark && mark.attrs.userid !== Doc.CurrentUserEmail) && [AclAugment, AclSelfEdit].includes(GetEffectiveAcl(this.Document))) {
                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 any).blur?.();
                SelectionManager.DeselectAll();
                RichTextMenu.Instance.updateMenu(undefined, undefined, undefined, undefined);
                return;
            case 'Enter':
                this.insertTime();
            case 'Tab':
                e.preventDefault();
                break;
            case 'c':
                this._editorView?.state.selection.empty && (stopPropagation = false);
                break;
            default:
                if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break;
            case ' ':
                if (e.code !== 'Space' && e.code !== 'Backspace') {
                    [AclEdit, AclAugment, AclAdmin].includes(GetEffectiveAcl(this.Document)) &&
                        this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })));
                }
                break;
        }
        if (stopPropagation) 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.current) {
            if (!this._props.dontSelectOnLoad) {
                this._ignoreScroll = true;
                this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop;
                this._ignoreScroll = false;
                e.stopPropagation();
                e.preventDefault();
            }
        }
    };
    tryUpdateScrollHeight = () => {
        const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
        const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
        if (children && !SnappingManager.IsDragging) {
            const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0'));
            const toHgt = (node: Element) => {
                const { height, marginTop, marginBottom } = getComputedStyle(node);
                return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom));
            };
            const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), 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.dataDoc[this.fieldKey + '_scrollHeight'] = scrollHeight);

                if (this.Document === this.layoutDoc || this.layoutDoc.resolvedDataDoc) {
                    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);
    sidebarContentScaling = () => (this._props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 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.dataDoc[this.SidebarKey + '_height'] = height);
    sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this._props.PanelWidth();
    sidebarScreenToLocal = () =>
        this._props
            .ScreenToLocalTransform()
            .translate(-(this._props.PanelWidth() - this.sidebarWidth()) / (this._props.NativeDimScaling?.() || 1), 0)
            .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this._props.NativeDimScaling?.() || 1));

    @computed get audioHandle() {
        return !this._recordingDictation ? null : (
            <div
                className="formattedTextBox-dictation"
                onPointerDown={e =>
                    setupMoveUpEvents(
                        this,
                        e,
                        returnFalse,
                        emptyFunction,
                        action(e => (this._recordingDictation = !this._recordingDictation))
                    )
                }>
                <FontAwesomeIcon className="formattedTextBox-audioFont" style={{ color: 'red' }} icon={'microphone'} size="sm" />
            </div>
        );
    }
    @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' : ''));

        return !annotated && (!this._props.isContentActive() || SnappingManager.IsDragging || Doc.ActiveTool !== InkTool.None) ? null : (
            <div
                className="formattedTextBox-sidebar-handle"
                onPointerDown={this.sidebarDown}
                style={{
                    backgroundColor,
                    color,
                    opacity: annotated ? 1 : undefined,
                }}>
                <FontAwesomeIcon icon="comment-alt" />
            </div>
        );
    }
    @computed get sidebarCollection() {
        const renderComponent = (tag: string) => {
            const ComponentTag: any = tag === CollectionViewType.Freeform ? CollectionFreeFormView : tag === CollectionViewType.Tree ? CollectionTreeView : tag === 'translation' ? FormattedTextBox : CollectionStackingView;
            return ComponentTag === CollectionStackingView ? (
                <SidebarAnnos
                    ref={this._sidebarRef}
                    {...this._props}
                    Document={this.Document}
                    layoutDoc={this.layoutDoc}
                    dataDoc={this.dataDoc}
                    usePanelWidth={true}
                    nativeWidth={NumCast(this.layoutDoc._nativeWidth)}
                    showSidebar={this.SidebarShown}
                    whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                    sidebarAddDocument={this.sidebarAddDocument}
                    moveDocument={this.moveDocument}
                    removeDocument={this.removeDocument}
                    ScreenToLocalTransform={this.sidebarScreenToLocal}
                    fieldKey={this.fieldKey}
                    PanelWidth={this.sidebarWidth}
                    setHeight={this.setSidebarHeight}
                />
            ) : (
                <div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => SelectionManager.SelectView(this.DocumentView?.()!, false), true)}>
                    <ComponentTag
                        {...this._props}
                        ref={this._sidebarTagRef as any}
                        setContentView={emptyFunction}
                        NativeWidth={returnZero}
                        NativeHeight={returnZero}
                        PanelHeight={this._props.PanelHeight}
                        PanelWidth={this.sidebarWidth}
                        xPadding={0}
                        yPadding={0}
                        viewField={this.SidebarKey}
                        isAnnotationOverlay={false}
                        select={emptyFunction}
                        isAnyChildContentActive={returnFalse}
                        NativeDimScaling={this.sidebarContentScaling}
                        whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
                        removeDocument={this.sidebarRemDocument}
                        moveDocument={this.sidebarMoveDocument}
                        addDocument={this.sidebarAddDocument}
                        ScreenToLocalTransform={this.sidebarScreenToLocal}
                        renderDepth={this._props.renderDepth + 1}
                        setHeight={this.setSidebarHeight}
                        fitContentsToBox={this.fitContentsToBox}
                        noSidebar={true}
                        treeViewHideTitle={true}
                        fieldKey={this.layoutDoc[this.SidebarKey + '_type_collection'] === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`}
                    />
                </div>
            );
        };
        return (
            <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
                {renderComponent(StrCast(this.layoutDoc[this.SidebarKey + '_type_collection']))}
            </div>
        );
    }
    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 (
            <Tooltip
                title={
                    <div className="dash-tooltip">
                        toggle (%/) between
                        <span style={{ color: usePath === undefined ? 'black' : undefined }}>
                            <em> primary, </em>
                        </span>
                        <span style={{ color: usePath === 'alternate' ? 'black' : undefined }}>
                            <em>alternate, </em>
                        </span>
                        and show
                        <span style={{ color: usePath === 'alternate:hover' ? 'black' : undefined }}>
                            <em> alternate on hover</em>
                        </span>
                    </div>
                }>
                <div
                    className="formattedTextBox-alternateButton"
                    onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => this.cycleAlternateText())}
                    style={{
                        display: this._props.isContentActive() && !SnappingManager.IsDragging ? 'flex' : 'none',
                        background: usePath === undefined ? 'white' : usePath === 'alternate' ? 'black' : 'gray',
                        color: usePath === undefined ? 'black' : 'white',
                    }}>
                    <FontAwesomeIcon icon="turn-up" size="sm" />
                </div>
            </Tooltip>
        );
    }
    get fieldKey() {
        return this._fieldKey;
    }
    @computed get _fieldKey() {
        const usePath = this._props.ignoreUsePath ? '' : StrCast(this.layoutDoc[`${this._props.fieldKey}_usePath`]);
        return this._props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering || this._props.isContentActive()) ? `_${usePath.replace(':hover', '')}` : '');
    }
    @observable _isHovering = false;
    onPassiveWheel = (e: WheelEvent) => {
        if (e.clientX > this.ProseRef!.getBoundingClientRect().right) {
            if (this.dataDoc[this.SidebarKey + '_type_collection'] === CollectionViewType.Freeform) {
                // if the scrolled freeform is a child of the sidebar component, we need to let the event go through
                // so react can let the freeform view handle it. We prevent default to stop any containing views from scrolling
                e.preventDefault();
            }
            return;
        }

        // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this)
        if (this._props.isContentActive()) {
            const scale = this._props.NativeDimScaling?.() || 1;
            const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
            const height = Number(styleFromLayoutString.height?.replace('px', ''));
            // prevent default if selected || child is active  but this doc isn't scrollable
            if (
                (this._scrollRef.current?.scrollHeight ?? 0) <= Math.ceil((height ? height : this._props.PanelHeight()) / scale) && //
                (this._props.rootSelected?.() || this.isAnyChildContentActive())
            ) {
                e.preventDefault();
            }
            e.stopPropagation();
        }
    };
    _oldWheel: any;
    @computed get fontColor() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color);
    }
    @computed get fontSize() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontSize);
    }
    @computed get fontFamily() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontFamily);
    }
    @computed get fontWeight() {
        return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.FontWeight);
    }
    render() {
        TraceMobx();
        const scale = this._props.NativeDimScaling?.() || 1; // * NumCast(this.layoutDoc._freeform_scale, 1);
        const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
        setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
        const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0);
        const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
        const styleFromLayoutString = Doc.styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props.  e.g., <FormattedTextBox height='{this._headerHeight}px' >
        return styleFromLayoutString?.height === '0px' ? null : (
            <div
                className="formattedTextBox"
                onPointerEnter={action(() => (this._isHovering = true))}
                onPointerLeave={action(() => (this._isHovering = false))}
                ref={r => {
                    this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel);
                    this._oldWheel = r;
                    r?.addEventListener('wheel', this.onPassiveWheel, { passive: false });
                }}
                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,
                    ...styleFromLayoutString,
                }}>
                <div
                    className="formattedTextBox-cont"
                    ref={this._ref}
                    style={{
                        cursor: this._props.isContentActive() ? 'text' : undefined,
                        height: this._props.height ? 'max-content' : undefined,
                        overflow: this.layout_autoHeight ? 'hidden' : undefined,
                        pointerEvents: Doc.ActiveTool === InkTool.None && !this._props.onBrowseClickScript?.() ? undefined : 'none',
                    }}
                    onContextMenu={this.specificContextMenu}
                    onKeyDown={this.onKeyDown}
                    onFocus={this.onFocused}
                    onClick={this.onClick}
                    onPointerMove={e => this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)}
                    onBlur={this.onBlur}
                    onPointerUp={this.onPointerUp}
                    onPointerDown={this.onPointerDown}
                    onDoubleClick={this.onDoubleClick}>
                    <div
                        className="formattedTextBox-outer"
                        ref={this._scrollRef}
                        style={{
                            width: this._props.dontSelectOnLoad || this.noSidebar ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`,
                            overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined,
                        }}
                        onScroll={this.onScroll}
                        onDrop={this.ondrop}>
                        <div
                            className={`formattedTextBox-inner${rounded} ${this.layoutDoc._layout_centered ? 'centered' : ''}`}
                            ref={this.createDropTarget}
                            style={{
                                padding: StrCast(this.layoutDoc._textBoxPadding),
                                paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
                                paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX}px`),
                                paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
                                paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY}px`),
                            }}
                        />
                    </div>
                    {this.noSidebar || this._props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection}
                    {this.noSidebar || this.Document._layout_noSidebar || this._props.dontSelectOnLoad || this.Document._createDocOnCR || this.layoutDoc._chromeHidden ? null : this.sidebarHandle}
                    {this.audioHandle}
                    {this.layoutDoc._layout_enableAltContentUI && !this.layoutDoc._chromeHidden ? this.overlayAlternateIcon : null}
                </div>
            </div>
        );
    }
}
