diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 225 |
1 files changed, 144 insertions, 81 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 6192b6829..8e1698eba 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,25 +1,26 @@ +import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { isEqual } from "lodash"; -import { action, computed, IReactionDisposer, reaction, runInAction, observable, trace } from "mobx"; +import { action, computed, IReactionDisposer, observable, 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 { ReplaceStep } from 'prosemirror-transform'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state"; import { EditorView } from "prosemirror-view"; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym, AclAugment } from "../../../../fields/Doc"; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, DocListCast, DocListCastAsync, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from "../../../../fields/Doc"; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { PrefetchProxy } from '../../../../fields/Proxy'; import { RichTextField } from "../../../../fields/RichTextField"; import { RichTextUtils } from '../../../../fields/RichTextUtils'; -import { Cast, DateCast, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; +import { ComputedField } from '../../../../fields/ScriptField'; +import { Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types"; import { GetEffectiveAcl, TraceMobx } from '../../../../fields/util'; -import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, OmitKeys, returnZero, setupMoveUpEvents, smoothScroll, unimplementedFunction, Utils } from '../../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../../DocServer"; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -28,7 +29,8 @@ import { CurrentUserUtils } from '../../../util/CurrentUserUtils'; import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from "../../../util/DragManager"; -import { makeTemplate } from '../../../util/DropConverter'; +import { MakeTemplate } from '../../../util/DropConverter'; +import { LinkManager } from '../../../util/LinkManager'; import { SelectionManager } from "../../../util/SelectionManager"; import { SnappingManager } from '../../../util/SnappingManager'; import { undoBatch, UndoManager } from "../../../util/UndoManager"; @@ -38,10 +40,11 @@ import { ContextMenu } from '../../ContextMenu'; import { ContextMenuProps } from '../../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from "../../DocComponent"; import { DocumentButtonBar } from '../../DocumentButtonBar'; +import { Colors } from '../../global/globalEnums'; import { LightboxView } from '../../LightboxView'; import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { SidebarAnnos } from '../../SidebarAnnos'; import { StyleProp } from '../../StyleProvider'; -import { AudioBox } from '../AudioBox'; import { FieldView, FieldViewProps } from "../FieldView"; import { LinkDocPreview } from '../LinkDocPreview'; import { DashDocCommentView } from "./DashDocCommentView"; @@ -60,9 +63,6 @@ import { schema } from "./schema_rts"; import { SummaryView } from "./SummaryView"; import applyDevTools = require("prosemirror-dev-tools"); import React = require("react"); -import { SidebarAnnos } from '../../SidebarAnnos'; -import { Colors } from '../../global/globalEnums'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; const translateGoogleApi = require("translate-google-api"); export interface FormattedTextBoxProps { @@ -83,7 +83,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; public static LiveTextUndo: UndoManager.Batch | undefined; - static _highlights: string[] = ["Audio Tags", "Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; + static _globalHighlights: 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(); @@ -220,6 +220,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp return undefined; }); AnchorMenu.Instance.onMakeAnchor = this.getAnchor; + 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. @@ -244,8 +245,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - const tsel = this._editorView.state.selection.$from; - tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000))); const curText = state.doc.textBetween(0, state.doc.content.size, " \n"); const curTemp = this.layoutDoc.resolvedDataDoc ? Cast(this.layoutDoc[this.props.fieldKey], RichTextField) : undefined; // the actual text in the text box const curProto = Cast(Cast(this.dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype @@ -256,7 +255,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const removeSelection = (json: string | undefined) => json?.indexOf("\"storedMarks\"") === -1 ? json?.replace(/"selection":.*/, "") : json?.replace(/"selection":"\"storedMarks\""/, "\"storedMarks\""); - if (effectiveAcl === AclEdit || effectiveAcl === AclAdmin || effectiveAcl === AclSelfEdit) { + if ([AclEdit, AclAdmin, AclSelfEdit].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("#")) { @@ -284,6 +283,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.dataDoc[this.props.fieldKey] = undefined; this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); unchanged = false; } this._applyingChange = ""; @@ -346,37 +346,78 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + autoLink = () => { + if (this._editorView?.state.doc.textContent) { + const newAutoLinks = new Set<Doc>(); + const oldAutoLinks = DocListCast(this.props.Document.links).filter(link => link.linkRelationship === LinkManager.AutoKeywords); + 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); + DocListCast(CurrentUserUtils.MyPublishedDocs.data).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.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); + } + } + updateTitle = () => { + const title = StrCast(this.dataDoc.title); if (!this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing - StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.dataDoc["title-custom"] && + (title.startsWith("-") || title.startsWith("@")) && this._editorView && !this.dataDoc["title-custom"] && (Doc.LayoutFieldKey(this.rootDoc) === 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; - this.dataDoc.title = "-" + str.substr(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : ""); + 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 ? "..." : ""); + if (str.startsWith("@") && str.length > 1) { + Doc.AddDocToList(CurrentUserUtils.MyPublishedDocs, undefined, this.rootDoc); + } + } } } - // needs a better API for taking in a set of words with target documents instead of just one target - hyperlinkTerms = (terms: string[], target: Doc) => { - if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { - const res1 = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); - let tr = this._editorView.state.tr; - const flattened1: TextSelection[] = []; - res1.map(r => r.map(h => flattened1.push(h))); + // creates links between terms in a document and published documents (myPublishedDocs) that have titles starting with an '@' + hyperlinkTerm = (tr: any, target: Doc, newAutoLinks: Set<Doc>) => { + const editorView = this._editorView; + if (editorView && (editorView as any).docView && !Doc.AreProtosEqual(target, this.rootDoc)) { + const autoLinkTerm = StrCast(target.title).replace(/^@/, ""); + const flattened1 = this.findInNode(editorView, editorView.state.doc, autoLinkTerm); + var alink: Doc | undefined; flattened1.forEach((flat, i) => { - const flattened: TextSelection[] = []; - const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); - res.map(r => r.map(h => flattened.push(h))); + const flattened = this.findInNode(this._editorView!, this._editorView!.state.doc, autoLinkTerm); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; - const anchor = Docs.Create.TextanchorDocument(); - const alink = DocUtils.MakeLink({ doc: anchor }, { doc: target }, "automatic")!; - const allAnchors = [{ href: Doc.localServerPath(anchor), title: "a link", anchorId: anchor[Id] }]; - const link = this._editorView!.state.schema.marks.linkAnchor.create({ allAnchors, title: "auto link", location }); - tr = tr.addMark(flattened[i].from, flattened[i].to, link); + const splitter = editorView.state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); + const sel = flattened[i]; + 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 ?? (DocListCast(this.Document.links).find(link => + Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && + Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, + LinkManager.AutoKeywords)!); + newAutoLinks.add(alink); + const allAnchors = [{ href: Doc.localServerPath(target), title: "a link", anchorId: this.props.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", location: "add:right" }); + tr = tr.addMark(pos, pos + node.nodeSize, link); + } + }); + tr = tr.removeMark(sel.from, sel.to, splitter); }); - this._editorView.dispatch(tr); } + 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)) { @@ -454,7 +495,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp // embed document when dragg marked as embed } else if (de.embedKey) { const target = dragData.droppedDocuments[0]; - target._fitToBox = true; + target._fitContentsToBox = true; const node = schema.nodes.dashDoc.create({ width: target[WidthSym](), height: target[HeightSym](), @@ -514,35 +555,40 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } updateHighlights = () => { + const highlights = FormattedTextBox._globalHighlights; clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (FormattedTextBox._highlights.indexOf("Audio Tags") === -1) { + if (highlights.indexOf("Audio Tags") === -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "audiotag", { display: "none" }, ""); } - if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) { + if (highlights.indexOf("Text from Others") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" }); } - if (FormattedTextBox._highlights.indexOf("My Text") !== -1) { + if (highlights.indexOf("My Text") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" }); } - if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) { + if (highlights.indexOf("Todo Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "todo", { outline: "black solid 1px" }); } - if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) { + if (highlights.indexOf("Important Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "important", { "font-size": "larger" }); } - if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) { + if (highlights.indexOf("Bold Text") !== -1) { + addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror strong > span", { "font-size": "large" }, ""); + addStyleSheetRule(FormattedTextBox._userStyleSheet, ".formattedTextBox-inner-selected .ProseMirror :not(strong > span)", { "font-size": "0px" }, ""); + } + if (highlights.indexOf("Disagree Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "disagree", { "text-decoration": "line-through" }); } - if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { + if (highlights.indexOf("Ignore Items") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "UT-" + "ignore", { "font-size": "1" }); } - if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { + if (highlights.indexOf("By Recent Minute") !== -1) { 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() })); setTimeout(this.updateHighlights); } - if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { + if (highlights.indexOf("By Recent Hour") !== -1) { 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() })); @@ -599,29 +645,29 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }), icon: icon }); }); - !Doc.UserDoc().noviceMode && changeItems.push({ description: "FreeForm", event: () => DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), icon: "eye" }); const highlighting: ContextMenuProps[] = []; - const noviceHighlighting = ["Audio Tags", "My Text", "Text from Others"]; + 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.UserDoc().noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => + (Doc.noviceMode ? noviceHighlighting : expertHighlighting).forEach(option => highlighting.push({ - description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { + description: (FormattedTextBox._globalHighlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => { e.stopPropagation(); - if (FormattedTextBox._highlights.indexOf(option) === -1) { - FormattedTextBox._highlights.push(option); + if (FormattedTextBox._globalHighlights.indexOf(option) === -1) { + FormattedTextBox._globalHighlights.push(option); } else { - FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1); + FormattedTextBox._globalHighlights.splice(FormattedTextBox._globalHighlights.indexOf(option), 1); } + runInAction(() => this.layoutDoc._highlights = FormattedTextBox._globalHighlights.join("")); this.updateHighlights(); }, icon: "expand-arrows-alt" })); const uicontrols: ContextMenuProps[] = []; - !Doc.UserDoc().noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ""} Show Menu on Selections`, event: () => FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate, icon: "expand-arrows-alt" }); + !Doc.noviceMode && uicontrols.push({ description: `${FormattedTextBox._canAnnotate ? "Don't" : ""} Show Menu on Selections`, event: () => FormattedTextBox._canAnnotate = !FormattedTextBox._canAnnotate, icon: "expand-arrows-alt" }); uicontrols.push({ description: !this.Document._noSidebar ? "Hide Sidebar Handle" : "Show Sidebar Handle", event: () => this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar, icon: "expand-arrows-alt" }); uicontrols.push({ description: `${this.layoutDoc._showAudio ? "Hide" : "Show"} Dictation Icon`, event: () => this.layoutDoc._showAudio = !this.layoutDoc._showAudio, icon: "expand-arrows-alt" }); uicontrols.push({ description: "Show Highlights...", noexpand: true, subitems: highlighting, icon: "hand-point-right" }); - !Doc.UserDoc().noviceMode && uicontrols.push({ + !Doc.noviceMode && uicontrols.push({ description: "Broadcast Message", event: () => DocServer.GetRefField("rtfProto").then(proto => proto instanceof Doc && (proto.BROADCAST_MESSAGE = Cast(this.rootDoc[this.fieldKey], RichTextField)?.Text)), icon: "expand-arrows-alt" }); @@ -631,12 +677,12 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const appearanceItems = appearance && "subitems" in appearance ? appearance.subitems : []; appearanceItems.push({ description: "Change Perspective...", noexpand: true, subitems: changeItems, icon: "external-link-alt" }); // this.rootDoc.isTemplateDoc && appearanceItems.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc), icon: "eye" }); - !Doc.UserDoc().noviceMode && appearanceItems.push({ + !Doc.noviceMode && appearanceItems.push({ description: "Make Default Layout", event: () => { if (!this.layoutDoc.isTemplateDoc) { const title = StrCast(this.rootDoc.title); this.rootDoc.title = "text"; - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + MakeTemplate(this.rootDoc, true, title); } else if (!this.rootDoc.isTemplateDoc) { const title = StrCast(this.rootDoc.title); this.rootDoc.title = "text"; @@ -645,7 +691,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this.rootDoc.isTemplateDoc = false; this.rootDoc.isTemplateForField = ""; this.rootDoc.layoutKey = "layout"; - this.rootDoc.isTemplateDoc = makeTemplate(this.rootDoc, true, title); + MakeTemplate(this.rootDoc, true, title); setTimeout(() => { this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template @@ -721,6 +767,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + // TODO: nda -- Look at how link anchors are added makeLinkAnchor(anchorDoc?: Doc, location?: string, targetHref?: string, title?: string) { const state = this._editorView?.state; if (state) { @@ -728,7 +775,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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.TextanchorDocument({ title: this._editorView?.state.doc.textBetween(sel.from, sel.to), unrendered: true }); + const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: "#" + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { @@ -784,6 +831,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }; 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); @@ -803,9 +851,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } - return this._focusSpeed; + return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed } + getScrollHeight = () => this.scrollHeight; // if the scroll height has changed and we're in 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) => { @@ -814,6 +863,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (nh) this.layoutDoc._nativeHeight = scrollHeight; } + @computed get contentScaling() { return Doc.NativeAspect(this.rootDoc, this.dataDoc, false) ? this.props.scaling?.() || 1 : 1;} componentDidMount() { !this.props.dontSelectOnLoad && this.props.setContentView?.(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 = DocListCast(this.Document.links); @@ -827,9 +877,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.componentHeights = reaction( // set the document height when one of the component heights changes and autoHeight is on () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { - autoHeight && this.props.setHeight?.(marginsHeight + Math.max(sidebarHeight, textHeight)); + autoHeight && this.props.setHeight?.(this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight))); }, { fireImmediately: true }); - this._disposers.links = reaction(() => DocListCast(this.Document.links), // if a link is deleted, then remove all hyperlinks that reference it from the text's marks + this._disposers.links = reaction(() => DocListCast(this.dataDoc.links), // 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; @@ -889,11 +939,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._disposers.selected = reaction(() => this.props.isSelected(), action(selected => { + this.layoutDoc._highlights = selected ? FormattedTextBox._globalHighlights.join("") : ""; if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); } if (this._editorView && selected) { RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + this.autoLink(); } }), { fireImmediately: true }); @@ -925,7 +977,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp }, { fireImmediately: true } ); quickScroll = undefined; - setTimeout(this.tryUpdateScrollHeight, 10); + this.tryUpdateScrollHeight(); } pushToGoogleDoc = async () => { @@ -1098,6 +1150,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } }); } + _didScroll = false; setupEditor(config: any, fieldKey: string) { const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null) || StrCast(this.dataDoc[this.props.fieldKey]); const rtfField = Cast((!curText && this.layoutDoc[this.props.fieldKey]) || this.dataDoc[fieldKey], RichTextField); @@ -1112,7 +1165,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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 || botOff) && scrollRef) { + 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.props.ScreenToLocalTransform().Scale; if (this._focusSpeed !== undefined) { @@ -1120,6 +1173,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else { scrollRef.scrollTo({ top: scrollPos }); } + this._didScroll = true; } return true; }, @@ -1154,19 +1208,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = ""; this.props.select(false); - if (FormattedTextBox.SelectOnLoadChar && this._editorView) { + if (selLoadChar && this._editorView) { 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 tr = this._editorView.state.tr.setStoredMarks(storedMarks).insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size).setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - FormattedTextBox.SelectOnLoadChar = ""; - } else if (curText && !FormattedTextBox.DontSelectInitialText) { - selectAll(this._editorView!.state, this._editorView?.dispatch); + } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) { + selectAll(this._editorView.state, this._editorView?.dispatch); this.startUndoTypingBatch(); + } else if (this._editorView) { + this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } FormattedTextBox.DontSelectInitialText = false; } @@ -1195,6 +1251,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } onPointerDown = (e: React.PointerEvent): void => { + if (this.Document.forceActive) e.stopPropagation(); this.tryUpdateScrollHeight(); // if a doc a fitwidth doc is being viewed in different context (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(); @@ -1256,7 +1313,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp !this.props.isSelected(true) && editor.dispatch(editor.state.tr.setSelection(new TextSelection(editor.state.doc.resolve(pcords?.pos || 0)))); let target = (e.target as any).parentElement; // hrefs are stored on the database 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); + FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); } (e.nativeEvent as any).formattedHandled = true; @@ -1356,9 +1413,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp const listNode = this._editorView?.state.doc.nodeAt(clickPos.pos); if (olistNode && olistNode.type === this._editorView?.state.schema.nodes.ordered_list && listNode) { if (!highlightOnly) { - if (selectOrderedList || (!collapse && listNode.attrs.visibility)) { + if (selectOrderedList) { this._editorView.dispatch(this._editorView.state.tr.setSelection(new NodeSelection(selectOrderedList ? $olistPos! : listPos!))); - } else if (!listNode.attrs.visibility || downNode === listNode) { + } else { const tr = this._editorView.state.tr.setNodeMarkup(clickPos.pos, listNode.type, { ...listNode.attrs, visibility: !listNode.attrs.visibility }); this._editorView.dispatch(tr.setSelection(TextSelection.create(tr.doc, clickPos.pos))); } @@ -1399,6 +1456,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp @action onBlur = (e: any) => { + this.autoLink(); FormattedTextBox.Focused === this && (FormattedTextBox.Focused = undefined); if (RichTextMenu.Instance?.view === this._editorView && !this.props.isSelected(true)) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); @@ -1421,12 +1479,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } catch (e: any) { console.log(e.message); } this._lastText = curText; } + if (StrCast(this.rootDoc.title).startsWith("@") && !this.dataDoc["title-custom"]) { + UndoManager.RunInBatch(() => { + this.dataDoc["title-custom"] = true; + this.dataDoc.showTitle = "title"; + const tr = this._editorView!.state.tr; + this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection()); + }, "titler"); + } } + onKeyDown = (e: React.KeyboardEvent) => { - // single line text boxes need to pass through tab/enter/backspace so that their containers can respond (eg, an outline container) - if (this.rootDoc._singleLine && ((e.key === "Backspace" && !this.dataDoc[this.fieldKey]?.Text) || ["Tab", "Enter"].includes(e.key))) { - return; - } if (e.altKey) { e.preventDefault(); return; @@ -1443,9 +1506,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp this._rules!.EnteringStyle = false; } e.stopPropagation(); - for (var i = state.selection.from; i < state.selection.to; i++) { + for (var i = state.selection.from; i <= state.selection.to; i++) { const node = state.doc.resolve(i); - if (node?.marks?.().some(mark => mark.type === schema.marks.user_mark && + 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.rootDoc))) { e.preventDefault(); @@ -1462,7 +1525,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp case "Tab": e.preventDefault(); break; default: if (this._lastTimedMark?.attrs.userid === Doc.CurrentUserEmail) break; case " ": - this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})) + [AclEdit, AclAdmin, AclSelfEdit].includes(GetEffectiveAcl(this.dataDoc)) && this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})) .addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } this.startUndoTypingBatch(); @@ -1486,7 +1549,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp if (children) { const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace("px", "")), margins); const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); - if (scrollHeight && this.props.renderDepth && !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 + if (this.props.setHeight && scrollHeight && this.props.renderDepth && !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.rootDoc[this.fieldKey + "-scrollHeight"] = scrollHeight; if (this.rootDoc === this.layoutDoc.doc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); @@ -1496,7 +1559,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } } - fitToBox = () => this.props.Document._fitToBox; + fitContentsToBox = () => this.props.Document._fitContentsToBox; sidebarContentScaling = () => (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { if (!this.layoutDoc._showSidebar) this.toggleSidebar(); @@ -1571,22 +1634,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ScreenToLocalTransform={this.sidebarScreenToLocal} renderDepth={this.props.renderDepth + 1} setHeight={this.setSidebarHeight} - fitContentsToDoc={this.fitToBox} + fitContentsToBox={this.fitContentsToBox} noSidebar={true} fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />; }; - return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")} + return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.ActiveTool !== InkTool.None ? "-inking" : "")} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> {renderComponent(StrCast(this.layoutDoc.sidebarViewType))} </div>; } render() { TraceMobx(); - const selected = this.props.isSelected(); + const selected = this.props.isSelected() || this.Document.forceActive; const active = this.props.isContentActive(); const scale = (this.props.scaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false); + const interactive = (CurrentUserUtils.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); @@ -1608,7 +1671,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp <div className={`formattedTextBox-cont`} ref={this._ref} style={{ overflow: this.autoHeight ? "hidden" : undefined, - height: this.props.height || (this.autoHeight && this.props.renderDepth ? "max-content" : undefined), + height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? "max-content" : undefined), background: this.props.background ? this.props.background : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.color ? this.props.color : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), fontSize: this.props.fontSize ? this.props.fontSize : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), |