diff options
Diffstat (limited to 'src/client/views/nodes/formattedText/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 112 |
1 files changed, 78 insertions, 34 deletions
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 6192b6829..29117794e 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -1,25 +1,25 @@ +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 { Cast, 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'; @@ -29,6 +29,7 @@ import { DictationManager } from '../../../util/DictationManager'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from "../../../util/DragManager"; 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 +39,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 +62,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 { @@ -220,6 +219,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. @@ -245,7 +245,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp 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))); + //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 @@ -346,37 +346,68 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } + autoLink = () => { + if (this._editorView) { + 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(Doc.UserDoc().myPublishedDocs).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("@") ? "" : "-"; + this.dataDoc.title = prefix + str.substring(0, Math.min(40, str.length)) + (str.length > 40 ? "..." : ""); + if (str.startsWith("@") && str.length > 1) { + Doc.AddDocToList(Doc.UserDoc(), "myPublishedDocs", 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 documents which have a matching Id + 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); + 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 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.splitter.name)) { + 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; } highlightSearchTerms = (terms: string[], backward: boolean) => { if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) { @@ -728,7 +759,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 +815,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,7 +835,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } } - return this._focusSpeed; + return this._didScroll ? this._focusSpeed : undefined; // if we actually scrolled, then return some focusSpeed } // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc. @@ -829,7 +861,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { autoHeight && this.props.setHeight?.(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; @@ -894,6 +926,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } if (this._editorView && selected) { RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); + this.autoLink(); } }), { fireImmediately: true }); @@ -1098,6 +1131,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 +1146,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 +1154,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp } else { scrollRef.scrollTo({ top: scrollPos }); } + this._didScroll = true; } return true; }, @@ -1256,7 +1291,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; @@ -1399,6 +1434,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,6 +1457,14 @@ 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) @@ -1486,7 +1530,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(); |