diff options
Diffstat (limited to 'src/client/views/nodes/FormattedTextBox.tsx')
-rw-r--r-- | src/client/views/nodes/FormattedTextBox.tsx | 624 |
1 files changed, 384 insertions, 240 deletions
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx index 037c92a62..60842bcb0 100644 --- a/src/client/views/nodes/FormattedTextBox.tsx +++ b/src/client/views/nodes/FormattedTextBox.tsx @@ -1,6 +1,6 @@ import { library } from '@fortawesome/fontawesome-svg-core'; import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons'; -import _ from "lodash"; +import { isEqual } from "lodash"; import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction, trace } from "mobx"; import { observer } from "mobx-react"; import { baseKeymap } from "prosemirror-commands"; @@ -17,25 +17,23 @@ import { Copy, Id } from '../../../new_fields/FieldSymbols'; import { RichTextField } from "../../../new_fields/RichTextField"; import { RichTextUtils } from '../../../new_fields/RichTextUtils'; import { createSchema, makeInterface } from "../../../new_fields/Schema"; -import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types"; -import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } from '../../../Utils'; +import { Cast, NumCast, StrCast } from "../../../new_fields/Types"; +import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, returnOne } from '../../../Utils'; import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils'; import { DocServer } from "../../DocServer"; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DictationManager } from '../../util/DictationManager'; -import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from "../../util/DragManager"; import buildKeymap from "../../util/ProsemirrorExampleTransfer"; import { inpRules } from "../../util/RichTextRules"; -import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } from "../../util/RichTextSchema"; +import { DashDocCommentView, FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema"; import { SelectionManager } from "../../util/SelectionManager"; import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu"; import { TooltipTextMenu } from "../../util/TooltipTextMenu"; import { undoBatch, UndoManager } from "../../util/UndoManager"; -import { DocExtendableComponent } from "../DocComponent"; +import { DocAnnotatableComponent } from "../DocComponent"; import { DocumentButtonBar } from '../DocumentButtonBar'; -import { DocumentDecorations } from '../DocumentDecorations'; import { InkingControl } from "../InkingControl"; import { FieldView, FieldViewProps } from "./FieldView"; import "./FormattedTextBox.scss"; @@ -46,6 +44,11 @@ import { ContextMenu } from '../ContextMenu'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { documentSchema } from '../../../new_fields/documentSchemas'; import { AudioBox } from './AudioBox'; +import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; +import { InkTool } from '../../../new_fields/InkField'; +import { TraceMobx } from '../../../new_fields/util'; +import RichTextMenu from '../../util/RichTextMenu'; +import { DocumentDecorations } from '../DocumentDecorations'; library.add(faEdit); library.add(faSmile, faTextHeight, faUpload); @@ -70,40 +73,45 @@ const RichTextDocument = makeInterface(richTextSchema, documentSchema); type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; @observer -export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { +export class FormattedTextBox extends DocAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } public static blankState = () => EditorState.create(FormattedTextBox.Instance.config); public static Instance: FormattedTextBox; - private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined; + public static ToolTipTextMenu: TooltipTextMenu | undefined = undefined; + public ProseRef?: HTMLDivElement; private _ref: React.RefObject<HTMLDivElement> = React.createRef(); - private _proseRef?: HTMLDivElement; + private _scrollRef: React.RefObject<HTMLDivElement> = React.createRef(); private _editorView: Opt<EditorView>; private _applyingChange: boolean = false; - private _nodeClicked: any; private _searchIndex = 0; + private _sidebarMovement = 0; + private _lastX = 0; + private _lastY = 0; private _undoTyping?: UndoManager.Batch; private _searchReactionDisposer?: Lambda; private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>; private _reactionDisposer: Opt<IReactionDisposer>; - private _textReactionDisposer: Opt<IReactionDisposer>; private _heightReactionDisposer: Opt<IReactionDisposer>; private _rulesReactionDisposer: Opt<IReactionDisposer>; private _proxyReactionDisposer: Opt<IReactionDisposer>; private _pullReactionDisposer: Opt<IReactionDisposer>; private _pushReactionDisposer: Opt<IReactionDisposer>; + private _buttonBarReactionDisposer: Opt<IReactionDisposer>; private dropDisposer?: DragManager.DragDropDisposer; - @observable private _fontSize = 13; - @observable private _fontFamily = "Arial"; + @observable private _ruleFontSize = 0; + @observable private _ruleFontFamily = "Arial"; @observable private _fontAlign = ""; @observable private _entered = false; + + public static FocusedBox: FormattedTextBox | undefined; public static SelectOnLoad = ""; public static IsFragment(html: string) { return html.indexOf("data-pm-slice") !== -1; } public static GetHref(html: string): string { - let parser = new DOMParser(); - let parsedHtml = parser.parseFromString(html, 'text/html'); + 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; @@ -120,17 +128,17 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } public static getToolTip(ev: EditorView) { - return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev); + return this.ToolTipTextMenu ? this.ToolTipTextMenu : this.ToolTipTextMenu = new TooltipTextMenu(ev); } @undoBatch public setFontColor(color: string) { - let view = this._editorView!; + const view = this._editorView!; if (view.state.selection.from === view.state.selection.to) return false; if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) { this.layoutDoc.color = color; } - let colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); + const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color }); view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark)); return true; } @@ -138,6 +146,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F constructor(props: any) { super(props); FormattedTextBox.Instance = this; + this.updateHighlights(); } public get CurrentDiv(): HTMLDivElement { return this._ref.current!; } @@ -146,9 +155,9 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F doLinkOnDeselect() { Array.from(this.linkOnDeselect.entries()).map(entry => { - let key = entry[0]; - let value = entry[1]; - let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + const key = entry[0]; + const value = entry[1]; + const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); DocServer.GetRefField(value).then(doc => { DocServer.GetRefField(id).then(linkDoc => { this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value); @@ -163,40 +172,38 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F dispatchTransaction = (tx: Transaction) => { if (this._editorView) { - let metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); + const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata); if (metadata) { - let range = tx.selection.$from.blockRange(tx.selection.$to); + const range = tx.selection.$from.blockRange(tx.selection.$to); let text = range ? tx.doc.textBetween(range.start, range.end) : ""; let textEndSelection = tx.selection.to; for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { } text = text.substr(0, textEndSelection - range!.start); text = text.split(" ")[text.split(" ").length - 1]; - let split = text.split("::"); + const split = text.split("::"); if (split.length > 1 && split[1]) { - let key = split[0]; - let value = split[split.length - 1]; + const key = split[0]; + const value = split[split.length - 1]; this.linkOnDeselect.set(key, value); - let id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); - const link = this._editorView.state.schema.marks.link.create({ href: `http://localhost:1050/doc/${id}`, location: "onRight", title: value }); + const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key); + const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value }); const mval = this._editorView.state.schema.marks.metadataVal.create(); - let offset = (tx.selection.to === range!.end - 1 ? -1 : 0); + const offset = (tx.selection.to === range!.end - 1 ? -1 : 0); tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval); this.dataDoc[key] = value; } } const state = this._editorView.state.apply(tx); this._editorView.updateState(state); - if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) { - FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks); - } + (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks); - let tsel = this._editorView.state.selection.$from; + 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 * 5000 - 1000))); this._applyingChange = true; - this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n")); + this.extensionDoc && !this.extensionDoc.lastModified && (this.extensionDoc.backgroundColor = "lightGray"); this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now()))); - this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON())); + this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), state.doc.textBetween(0, state.doc.content.size, "\n\n")); this._applyingChange = false; this.updateTitle(); this.tryUpdateHeight(); @@ -205,21 +212,21 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F updateTitle = () => { if (StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.Document.customTitle) { - let str = this._editorView.state.doc.textContent; - let titlestr = str.substr(0, Math.min(40, str.length)); + const str = this._editorView.state.doc.textContent; + const titlestr = str.substr(0, Math.min(40, str.length)); this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : ""); } } public highlightSearchTerms = (terms: string[]) => { - if (this._editorView && (this._editorView as any).docView) { + 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 }); - let res = terms.map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); + const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term)); let tr = this._editorView.state.tr; - let flattened: TextSelection[] = []; + const flattened: TextSelection[] = []; res.map(r => r.map(h => flattened.push(h))); - let lastSel = Math.min(flattened.length - 1, this._searchIndex); + 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)); this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex; this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView()); @@ -230,59 +237,59 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F 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 }); - let end = this._editorView.state.doc.nodeSize - 2; + const end = this._editorView.state.doc.nodeSize - 2; this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark)); } } - setAnnotation = (start: number, end: number, mark: Mark, opened: boolean, keep: boolean = false) => { - let view = this._editorView!; - let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened }); + 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._proseRef = ele; + this.ProseRef = ele; this.dropDisposer && this.dropDisposer(); - ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } })); + ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this))); } @undoBatch @action drop = async (e: Event, de: DragManager.DropEvent) => { - if (de.data instanceof DragManager.DocumentDragData) { - const draggedDoc = de.data.draggedDocuments.length && de.data.draggedDocuments[0]; + if (de.complete.docDragData) { + const draggedDoc = de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0]; // replace text contents whend dragging with Alt - if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "AltKey") { + if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) { if (draggedDoc.data instanceof RichTextField) { - Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data); + Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text); e.stopPropagation(); } // apply as template when dragging with Meta - } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.mods === "MetaKey") { + } else if (draggedDoc && draggedDoc.type === DocumentType.TEXT && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.metaKey) { draggedDoc.isTemplateDoc = true; let newLayout = Doc.Layout(draggedDoc); if (typeof (draggedDoc.layout) === "string") { newLayout = Doc.MakeDelegate(draggedDoc); - newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={"[^"]*"}/, `fieldKey={"${this.props.fieldKey}"}`); + newLayout.layout = StrCast(newLayout.layout).replace(/fieldKey={'[^']*'}/, `fieldKey={'${this.props.fieldKey}'}`); } this.Document.layoutCustom = newLayout; this.Document.layoutKey = "layoutCustom"; e.stopPropagation(); // embed document when dragging with a userDropAction or an embedDoc flag set - } else if (de.data.userDropAction || de.data.embedDoc) { - let target = de.data.droppedDocuments[0]; - const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); - if (link) { - target.fitToBox = true; - let node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), height: target[HeightSym](), - title: "dashDoc", docid: target[Id], - float: "right" - }); - let view = this._editorView!; - view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); - this.tryUpdateHeight(); - e.stopPropagation(); - } + } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) { + const target = de.complete.docDragData.droppedDocuments[0]; + // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title); + // if (link) { + target.fitToBox = true; + const node = schema.nodes.dashDoc.create({ + width: target[WidthSym](), height: target[HeightSym](), + title: "dashDoc", docid: target[Id], + float: "right" + }); + const view = this._editorView!; + view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); + this.tryUpdateHeight(); + e.stopPropagation(); + // } } // otherwise, fall through to outer collection to handle drop } } @@ -293,8 +300,9 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F 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++) { - let result = this.getNodeEndpoints((context.content as any).content[i], node); + const result = this.getNodeEndpoints((context.content as any).content[i], node); if (result) { return { from: result.from + offset + (context.type.name === "doc" ? 0 : 1), @@ -315,9 +323,10 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F let ret: TextSelection[] = []; if (node.isTextblock) { - let index = 0, foundAt, ep = this.getNodeEndpoints(pm.state.doc, node); + let index = 0, foundAt; + const ep = this.getNodeEndpoints(pm.state.doc, node); while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) { - let sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); + const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1)); ret.push(sel); index = index + foundAt + find.length; } @@ -326,7 +335,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } return ret; } - static _highlights: string[] = []; + static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"]; updateHighlights = () => { clearStyleSheetRules(FormattedTextBox._userStyleSheet); @@ -346,23 +355,45 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" }); } if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) { - addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" }); + addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" }); } if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); - let min = Math.round(Date.now() / 1000 / 60); + const min = Math.round(Date.now() / 1000 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() })); setTimeout(() => this.updateHighlights()); } if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) { addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" }); - let hr = Math.round(Date.now() / 1000 / 60 / 60); + const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } } + sidebarDown = (e: React.PointerEvent) => { + this._lastX = e.clientX; + this._lastY = e.clientY; + this._sidebarMovement = 0; + document.addEventListener("pointermove", this.sidebarMove); + document.addEventListener("pointerup", this.sidebarUp); + e.stopPropagation(); + e.preventDefault(); // prevents text from being selected during drag + } + sidebarMove = (e: PointerEvent) => { + const bounds = this.CurrentDiv.getBoundingClientRect(); + this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY)); + this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%"; + } + sidebarUp = (e: PointerEvent) => { + document.removeEventListener("pointermove", this.sidebarMove); + document.removeEventListener("pointerup", this.sidebarUp); + } + + toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%"); + specificContextMenu = (e: React.MouseEvent): void => { - let funcs: ContextMenuProps[] = []; + const funcs: ContextMenuProps[] = []; + funcs.push({ description: "Toggle Sidebar", event: () => { e.stopPropagation(); this.toggleSidebar(); }, icon: "expand-arrows-alt" }); funcs.push({ description: "Record Bullet", event: () => { e.stopPropagation(); this.recordBullet(); }, icon: "expand-arrows-alt" }); ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option => funcs.push({ @@ -402,8 +433,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } recordBullet = async () => { - let completedCue = "end session"; - let results = await DictationManager.Controls.listen({ + const completedCue = "end session"; + const results = await DictationManager.Controls.listen({ interimHandler: this.setCurrentBulletContent, continuous: { indefinite: false }, terminators: [completedCue, "bullet", "next"] @@ -419,20 +450,20 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F setCurrentBulletContent = (value: string) => { if (this._editorView) { let state = this._editorView.state; - let from = state.selection.from; - let to = state.selection.to; + const from = state.selection.from; + const to = state.selection.to; this._editorView.dispatch(state.tr.insertText(value, from, to)); state = this._editorView.state; - let updated = TextSelection.create(state.doc, from, from + value.length); + const updated = TextSelection.create(state.doc, from, from + value.length); this._editorView.dispatch(state.tr.setSelection(updated)); } } nextBullet = (pos: number) => { if (this._editorView) { - let frag = Fragment.fromArray(this.newListItems(2)); + const frag = Fragment.fromArray(this.newListItems(2)); if (this._editorView.state.doc.resolve(pos).depth >= 2) { - let slice = new Slice(frag, 2, 2); + const slice = new Slice(frag, 2, 2); let state = this._editorView.state; this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice))); pos += 4; @@ -470,8 +501,15 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } componentDidMount() { - this.pullFromGoogleDoc(this.checkState); - this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true); + this._buttonBarReactionDisposer = reaction( + () => DocumentButtonBar.Instance, + instance => { + if (instance) { + this.pullFromGoogleDoc(this.checkState); + this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => instance.isAnimatingFetch = true); + } + } + ); this._reactionDisposer = reaction( () => { @@ -480,7 +518,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F }, incomingValue => { if (this._editorView && !this._applyingChange) { - let updatedState = JSON.parse(incomingValue); + const updatedState = JSON.parse(incomingValue); this._editorView.updateState(EditorState.fromJSON(this.config, updatedState)); this.tryUpdateHeight(); } @@ -492,7 +530,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F () => { if (!DocumentButtonBar.hasPulledHack) { DocumentButtonBar.hasPulledHack = true; - let unchanged = this.dataDoc.unchanged; + const unchanged = this.dataDoc.unchanged; this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState); } } @@ -513,35 +551,15 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F () => this.tryUpdateHeight() ); - this._textReactionDisposer = reaction( - () => this.extensionDoc, - () => { - if (this.extensionDoc && (this.dataDoc.text || this.dataDoc.lastModified)) { - this.extensionDoc.text = this.dataDoc.text; - this.extensionDoc.lastModified = DateCast(this.dataDoc.lastModified)[Copy](); - this.dataDoc.text = undefined; - this.dataDoc.lastModified = undefined; - } - }, { fireImmediately: true }); - - this.setupEditor(this.config, this.dataDoc, this.props.fieldKey); - this._searchReactionDisposer = reaction(() => { - return StrCast(this.layoutDoc.search_string); - }, searchString => { - if (searchString) { - this.highlightSearchTerms([searchString]); - } - else { - this.unhighlightSearchTerms(); - } - }, { fireImmediately: true }); - + this._searchReactionDisposer = reaction(() => this.layoutDoc.searchMatch, + search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(), + { fireImmediately: true }); this._rulesReactionDisposer = reaction(() => { - let ruleProvider = this.props.ruleProvider; - let heading = NumCast(this.layoutDoc.heading); + const ruleProvider = this.props.ruleProvider; + const heading = NumCast(this.layoutDoc.heading); if (ruleProvider instanceof Doc) { return { align: StrCast(ruleProvider["ruleAlign_" + heading], ""), @@ -552,12 +570,12 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F return undefined; }, action((rules: any) => { - this._fontFamily = rules ? rules.font : "Arial"; - this._fontSize = rules ? rules.size : NumCast(this.layoutDoc.fontSize, 13); + this._ruleFontFamily = rules ? rules.font : "Arial"; + this._ruleFontSize = rules ? rules.size : 0; rules && setTimeout(() => { const view = this._editorView!; - if (this._proseRef) { - let n = new NodeSelection(view.state.doc.resolve(0)); + if (this.ProseRef) { + const n = new NodeSelection(view.state.doc.resolve(0)); if (this._editorView!.state.doc.textContent === "") { view.dispatch(view.state.tr.setSelection(new TextSelection(view.state.doc.resolve(0), view.state.doc.resolve(2))). replaceSelectionWith(this._editorView!.state.schema.nodes.paragraph.create({ align: rules.align }), true)); @@ -572,10 +590,10 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F this._scrollToRegionReactionDisposer = reaction( () => StrCast(this.layoutDoc.scrollToLinkID), async (scrollToLinkID) => { - let findLinkFrag = (frag: Fragment, editor: EditorView) => { + const findLinkFrag = (frag: Fragment, editor: EditorView) => { const nodes: Node[] = []; frag.forEach((node, index) => { - let examinedNode = findLinkNode(node, editor); + const examinedNode = findLinkNode(node, editor); if (examinedNode && examinedNode.textContent) { nodes.push(examinedNode); start += index; @@ -583,7 +601,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F }); return { frag: Fragment.fromArray(nodes), start: start }; }; - let findLinkNode = (node: Node, editor: EditorView) => { + const findLinkNode = (node: Node, editor: EditorView) => { if (!node.isText) { const content = findLinkFrag(node.content, editor); return node.copy(content.frag); @@ -595,8 +613,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F let start = -1; if (this._editorView && scrollToLinkID) { - let editor = this._editorView; - let ret = findLinkFrag(editor.state.doc.content, editor); + const editor = this._editorView; + const ret = findLinkFrag(editor.state.doc.content, editor); if (ret.frag.size > 2 && ret.start >= 0) { let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start @@ -615,33 +633,33 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F { fireImmediately: true } ); - setTimeout(() => this.tryUpdateHeight(), 0); + setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0))); } pushToGoogleDoc = async () => { this.pullFromGoogleDoc(async (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { - let modes = GoogleApiClientUtils.Docs.WriteMode; + const modes = GoogleApiClientUtils.Docs.WriteMode; let mode = modes.Replace; let reference: Opt<GoogleApiClientUtils.Docs.Reference> = Cast(this.dataDoc[GoogleRef], "string"); if (!reference) { mode = modes.Insert; reference = { title: StrCast(this.dataDoc.title) }; } - let redo = async () => { + const redo = async () => { if (this._editorView && reference) { - let content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); - let response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); + const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state); + const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode }); response && (this.dataDoc[GoogleRef] = response.documentId); - let pushSuccess = response !== undefined && !("errors" in response); + const pushSuccess = response !== undefined && !("errors" in response); dataDoc.unchanged = pushSuccess; DocumentButtonBar.Instance.startPushOutcome(pushSuccess); } }; - let undo = () => { + const undo = () => { if (!exportState) { return; } - let content: GoogleApiClientUtils.Docs.Content = { + const content: GoogleApiClientUtils.Docs.Content = { text: exportState.text, requests: [] }; @@ -655,8 +673,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } pullFromGoogleDoc = async (handler: PullHandler) => { - let dataDoc = this.dataDoc; - let documentId = StrCast(dataDoc[GoogleRef]); + const dataDoc = this.dataDoc; + const documentId = StrCast(dataDoc[GoogleRef]); let exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>; if (documentId) { exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc); @@ -671,8 +689,8 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON())); setTimeout(() => { if (this._editorView) { - let state = this._editorView.state; - let end = state.doc.content.size - 1; + const state = this._editorView.state; + const end = state.doc.content.size - 1; this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end))); } }, 0); @@ -687,9 +705,9 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F checkState = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => { if (exportState && this._editorView) { - let equalContent = _.isEqual(this._editorView.state.doc, exportState.state.doc); - let equalTitles = dataDoc.title === exportState.title; - let unchanged = equalContent && equalTitles; + const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc); + const equalTitles = dataDoc.title === exportState.title; + const unchanged = equalContent && equalTitles; dataDoc.unchanged = unchanged; DocumentButtonBar.Instance.setPullState(unchanged); } @@ -717,7 +735,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => { - let cbe = event as ClipboardEvent; + const cbe = event as ClipboardEvent; const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin"); const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion"); if (pdfDocId && pdfRegionId) { @@ -727,18 +745,18 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F setTimeout(async () => { const extension = Doc.fieldExtensionDoc(pdfDoc, "data"); if (extension) { - let targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations + const targetAnnotations = await DocListCastAsync(extension.annotations);// bcz: NO... this assumes the pdf is using its 'data' field. need to have the PDF's view handle updating its own annotations targetAnnotations && targetAnnotations.push(pdfRegion); } }); - let link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); + const link = DocUtils.MakeLink({ doc: this.props.Document, ctx: this.props.ContainingCollectionDoc }, { doc: pdfRegion, ctx: pdfDoc }, "note on " + pdfDoc.title, "pasted PDF link"); if (link) { cbe.clipboardData!.setData("dash/linkDoc", link[Id]); - let linkId = link[Id]; - let frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); + const linkId = link[Id]; + const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId)); slice = new Slice(frag, slice.openStart, slice.openEnd); - var tr = view.state.tr.replaceSelection(slice); + const tr = view.state.tr.replaceSelection(slice); view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste")); } } @@ -768,54 +786,59 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } private setupEditor(config: any, doc: Doc, fieldKey: string) { - let field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; + const field = doc ? Cast(doc[fieldKey], RichTextField) : undefined; let startup = StrCast(doc.documentText); startup = startup.startsWith("@@@") ? startup.replace("@@@", "") : ""; if (!field && doc) { - let text = StrCast(doc[fieldKey]); + const text = StrCast(doc[fieldKey]); if (text) { startup = text; } else if (Cast(doc[fieldKey], "number")) { startup = NumCast(doc[fieldKey], 99).toString(); } } - if (this._proseRef) { - let self = this; + if (this.ProseRef) { + const self = this; this._editorView && this._editorView.destroy(); - this._editorView = new EditorView(this._proseRef, { + this._editorView = new EditorView(this.ProseRef, { state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config), handleScrollToSelection: (editorView) => { - let ref = editorView.domAtPos(editorView.state.selection.from); + const ref = editorView.domAtPos(editorView.state.selection.from); let refNode = ref.node as any; while (refNode && !("getBoundingClientRect" in refNode)) refNode = refNode.parentElement; - let r1 = refNode && refNode.getBoundingClientRect(); - let r3 = self._ref.current!.getBoundingClientRect(); - r1 && (self._ref.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); + const r1 = refNode && refNode.getBoundingClientRect(); + const r3 = self._ref.current!.getBoundingClientRect(); + if (r1.top < r3.top || r1.top > r3.bottom) { + r1 && (self._scrollRef.current!.scrollTop += (r1.top - r3.top) * self.props.ScreenToLocalTransform().Scale); + } return true; }, dispatchTransaction: this.dispatchTransaction, nodeViews: { + dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); }, dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); }, image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); }, - star(node, view, getPos) { return new SummarizedView(node, view, getPos); }, + summary(node, view, getPos) { return new SummaryView(node, view, getPos); }, ordered_list(node, view, getPos) { return new OrderedListView(); }, footnote(node, view, getPos) { return new FootnoteView(node, view, getPos); } }, clipboardTextSerializer: this.clipboardTextSerializer, handlePaste: this.handlePaste, }); - if (startup) { + this._editorView.state.schema.Document = this.props.Document; + if (startup && this._editorView) { Doc.GetProto(doc).documentText = undefined; this._editorView.dispatch(this._editorView.state.tr.insertText(startup)); } } - let selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; + const selectOnLoad = this.props.Document[Id] === FormattedTextBox.SelectOnLoad; if (selectOnLoad) { FormattedTextBox.SelectOnLoad = ""; this.props.select(false); } - this._editorView!.focus(); + const rtf = doc ? Cast(doc[fieldKey], RichTextField) : undefined; + (selectOnLoad || (rtf && !rtf.Text)) && this._editorView!.focus(); // 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. this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) })]; } @@ -837,21 +860,26 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F this._rulesReactionDisposer && this._rulesReactionDisposer(); this._reactionDisposer && this._reactionDisposer(); this._proxyReactionDisposer && this._proxyReactionDisposer(); - this._textReactionDisposer && this._textReactionDisposer(); this._pushReactionDisposer && this._pushReactionDisposer(); this._pullReactionDisposer && this._pullReactionDisposer(); this._heightReactionDisposer && this._heightReactionDisposer(); this._searchReactionDisposer && this._searchReactionDisposer(); + this._buttonBarReactionDisposer && this._buttonBarReactionDisposer(); this._editorView && this._editorView.destroy(); } + + static _downEvent: any; onPointerDown = (e: React.PointerEvent): void => { - let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - pos && (this._nodeClicked = this._editorView!.state.doc.nodeAt(pos.pos)); + this.doLinkOnDeselect(); + FormattedTextBox._downEvent = true; + FormattedTextBoxComment.textBox = this; if (this.props.onClick && e.button === 0) { e.preventDefault(); } - if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) { - e.stopPropagation(); + if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) { + if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar + e.stopPropagation(); + } } if (e.button === 2 || (e.button === 0 && e.ctrlKey)) { e.preventDefault(); @@ -859,23 +887,42 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } onPointerUp = (e: React.PointerEvent): void => { - if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; } + if (!FormattedTextBox._downEvent) return; + FormattedTextBox._downEvent = false; + if (!(e.nativeEvent as any).formattedHandled) { + FormattedTextBoxComment.textBox = this; + FormattedTextBoxComment.update(this._editorView!); + } (e.nativeEvent as any).formattedHandled = true; - if (e.buttons === 1 && this.props.isSelected() && !e.altKey) { + if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) { e.stopPropagation(); } } - static InputBoxOverlay: FormattedTextBox | undefined; @action onFocused = (e: React.FocusEvent): void => { - FormattedTextBox.InputBoxOverlay = this; + FormattedTextBox.FocusedBox = this; this.tryUpdateHeight(); + + // see if we need to preserve the insertion point + const prosediv = this.ProseRef?.children?.[0] as any; + const keeplocation = prosediv?.keeplocation; + prosediv && (prosediv.keeplocation = undefined); + const pos = this._editorView?.state.selection.$from.pos || 1; + keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos)))); + + // jump rich text menu to this textbox + const { current } = this._ref; + if (current) { + const x = Math.min(Math.max(current.getBoundingClientRect().left, 0), window.innerWidth - RichTextMenu.Instance.width); + const y = this._ref.current!.getBoundingClientRect().top - RichTextMenu.Instance.height - 50; + RichTextMenu.Instance.jumpTo(x, y); + } } onPointerWheel = (e: React.WheelEvent): void => { // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time - if (this.props.isSelected() || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { + if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) { e.stopPropagation(); } } @@ -884,66 +931,91 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F static _userStyleSheet: any = addStyleSheet(); onClick = (e: React.MouseEvent): void => { - if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } - (e.nativeEvent as any).formattedHandled = true; - if (e.button === 0 && ((!this.props.isSelected() && !e.ctrlKey) || (this.props.isSelected() && e.ctrlKey)) && !e.metaKey && e.target) { - let href = (e.target as any).href; - let location: string; - if ((e.target as any).attributes.location) { - location = (e.target as any).attributes.location.value; + if ((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(); } - let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); - let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); - if (node) { - let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link); - if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above). - href = link && link.attrs.href; - location = link && link.attrs.location; + 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 + if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size))); } } - if (href) { - if (href.indexOf(Utils.prepend("/doc/")) === 0) { - let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; - if (linkClicked) { - DocServer.GetRefField(linkClicked).then(async linkDoc => { - (linkDoc instanceof Doc) && - DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false); - }); - } - } else { - let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) }); - this.props.addDocument && this.props.addDocument(webDoc); - } - e.stopPropagation(); - e.preventDefault(); - } } - - this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey); + if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; } + (e.nativeEvent as any).formattedHandled = true; + // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) { + // let href = (e.target as any).href; + // let location: string; + // if ((e.target as any).attributes.location) { + // location = (e.target as any).attributes.location.value; + // } + // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY }); + // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); + // if (node) { + // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link); + // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above). + // href = link && link.attrs.href; + // location = link && link.attrs.location; + // } + // } + // if (href) { + // if (href.indexOf(Utils.prepend("/doc/")) === 0) { + // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0]; + // if (linkClicked) { + // DocServer.GetRefField(linkClicked).then(async linkDoc => { + // (linkDoc instanceof Doc) && + // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), false); + // }); + // } + // } else { + // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) }); + // this.props.addDocument && this.props.addDocument(webDoc); + // } + // e.stopPropagation(); + // e.preventDefault(); + // } + // } + + this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false); if (this._recording) setTimeout(() => { this.stopDictation(true); setTimeout(() => this.recordDictation(), 500); }, 500); } // 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, offsetX: number, select: boolean = false) { + hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) { clearStyleSheetRules(FormattedTextBox._bulletStyleSheet); - if (this.props.isSelected() && offsetX < 40) { - let pos = this._editorView!.posAtCoords({ left: x, top: y }); - if (pos && pos.pos > 0) { - let node = this._editorView!.state.doc.nodeAt(pos.pos); - let node2 = node && node.type === schema.nodes.paragraph ? this._editorView!.state.doc.nodeAt(pos.pos - 1) : undefined; - if (node === this._nodeClicked && node2 && (node2.type === schema.nodes.ordered_list || node2.type === schema.nodes.list_item)) { - let hit = this._editorView!.domAtPos(pos.pos).node as any; // let beforeEle = document.querySelector("." + hit.className) as Element; - let before = hit ? window.getComputedStyle(hit, ':before') : undefined; - let beforeWidth = before ? Number(before.getPropertyValue('width').replace("px", "")) : undefined; - if (beforeWidth && offsetX < beforeWidth) { - let ol = this._editorView!.state.doc.nodeAt(pos.pos - 2) ? this._editorView!.state.doc.nodeAt(pos.pos - 2) : undefined; - if (ol && ol.type === schema.nodes.ordered_list && select) { - this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection(this._editorView!.state.doc.resolve(pos.pos - 2)))); - addStyleSheetRule(FormattedTextBox._bulletStyleSheet, hit.className + ":before", { background: "gray" }); - } else { - this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.pos - 1, node2.type, { ...node2.attrs, visibility: !node2.attrs.visibility })); - } + const pos = this._editorView!.posAtCoords({ left: x, top: y }); + if (pos && this.props.isSelected(true)) { + // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined; + //const node = this._editorView!.state.doc.nodeAt(pos.pos); + const $pos = this._editorView!.state.doc.resolve(pos.pos); + let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined; + if ($pos.node().type === schema.nodes.ordered_list) { + for (let off = 1; off < 100; off++) { + const pos = this._editorView!.posAtCoords({ left: x + off, top: y }); + const node = pos && this._editorView!.state.doc.nodeAt(pos.pos); + if (node?.type === schema.nodes.list_item) { + list_node = node; + break; + } + } + } + if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) { + if (select) { + const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1); + if (!highlightOnly) { + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos))); } + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); + } else if (Math.abs(pos.pos - pos.inside) < 2) { + if (!highlightOnly) { + this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility })); + this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside))); + } + addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" }); } } } @@ -951,11 +1023,11 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F onMouseUp = (e: React.MouseEvent): void => { e.stopPropagation(); - let view = this._editorView as any; + const view = this._editorView as any; // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there // are nested prosemirrors. We only want the lowest level prosemirror to be invoked. if (view.mouseDown) { - let originalUpHandler = view.mouseDown.up; + const originalUpHandler = view.mouseDown.up; view.root.removeEventListener("mouseup", originalUpHandler); view.mouseDown.up = (e: MouseEvent) => { !(e as any).formattedHandled && originalUpHandler(e); @@ -967,16 +1039,18 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F } tooltipTextMenuPlugin() { - let self = FormattedTextBox; + const self = FormattedTextBox; return new Plugin({ view(newView) { - return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView); + // return self.ToolTipTextMenu = FormattedTextBox.getToolTip(newView); + RichTextMenu.Instance.changeView(newView); + return RichTextMenu.Instance; } }); } tooltipLinkingMenuPlugin() { - let myprops = this.props; + const myprops = this.props; return new Plugin({ view(_editorView) { return new TooltipLinkingMenu(_editorView, myprops); @@ -990,42 +1064,83 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F this._undoTyping = undefined; } this.doLinkOnDeselect(); + + // move the richtextmenu offscreen + if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300); } + + _lastTimedMark: Mark | undefined = undefined; onKeyPress = (e: React.KeyboardEvent) => { + if (e.altKey) { + e.preventDefault(); + return; + } + const state = this._editorView!.state; + if (!state.selection.empty && e.key === "%") { + state.schema.EnteringStyle = true; + e.preventDefault(); + e.stopPropagation(); + return; + } + + if (state.selection.empty || !state.schema.EnteringStyle) { + state.schema.EnteringStyle = false; + } if (e.key === "Escape") { + this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from))); + (document.activeElement as any).blur?.(); SelectionManager.DeselectAll(); } e.stopPropagation(); if (e.key === "Tab" || e.key === "Enter") { e.preventDefault(); } - let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) }); + this._lastTimedMark = mark; this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark)); if (!this._undoTyping) { this._undoTyping = UndoManager.StartBatch("undoTyping"); } - if (this._recording) { this.stopDictation(true); setTimeout(() => this.recordDictation(), 250); } + if (this._recording) { + this.stopDictation(true); + setTimeout(() => this.recordDictation(), 250); + } } @action - tryUpdateHeight() { - let scrollHeight = this._ref.current ? this._ref.current.scrollHeight : 0; - if (!this.layoutDoc.isAnimating && this.layoutDoc.autoHeight && scrollHeight !== 0 && + tryUpdateHeight(limitHeight?: number) { + let scrollHeight = this._ref.current?.scrollHeight; + if (!this.layoutDoc.animateToPos && this.layoutDoc.autoHeight && scrollHeight && getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - let nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); - let dh = NumCast(this.layoutDoc.height, 0); - this.layoutDoc.height = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); - this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; + if (limitHeight && scrollHeight > limitHeight) { + scrollHeight = limitHeight; + this.layoutDoc.limitHeight = undefined; + this.layoutDoc.autoHeight = false; + } + const nh = this.Document.isTemplateField ? 0 : NumCast(this.dataDoc.nativeHeight, 0); + const dh = NumCast(this.layoutDoc.height, 0); + const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0)); + if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle + this.layoutDoc.height = newHeight; + this.dataDoc.nativeHeight = nh ? scrollHeight : undefined; + } } } + @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); } + @computed get sidebarWidth() { return Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth(); } + @computed get annotationsKey() { return "annotations"; } render() { - let rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; - let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground - ? "none" : "all"; + TraceMobx(); + const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : ""; + const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground; if (this.props.isSelected()) { - FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + // TODO: ftong --> update from dash in richtextmenu + RichTextMenu.Instance.updateFromDash(this._editorView!, undefined, this.props); + // FormattedTextBox.ToolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props); + } else if (FormattedTextBoxComment.textBox === this) { + FormattedTextBoxComment.Hide(); } return ( <div className={`formattedTextBox-cont`} ref={this._ref} @@ -1034,14 +1149,15 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined, opacity: this.props.hideOnLeave ? (this._entered ? 1 : 0.1) : 1, color: this.props.color ? this.props.color : this.props.hideOnLeave ? "white" : "inherit", - pointerEvents: interactive, - fontSize: this._fontSize, - fontFamily: this._fontFamily, + pointerEvents: interactive ? "none" : "all", + fontSize: this._ruleFontSize ? this._ruleFontSize : NumCast(this.layoutDoc.fontSize, 13), + fontFamily: this._ruleFontFamily ? this._ruleFontFamily : StrCast(this.layoutDoc.fontFamily, "Crimson Text"), }} onContextMenu={this.specificContextMenu} onKeyDown={this.onKeyPress} 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} @@ -1050,8 +1166,36 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F onPointerEnter={action(() => this._entered = true)} onPointerLeave={action(() => this._entered = false)} > - <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> - + <div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} ref={this._scrollRef}> + <div className={`formattedTextBox-inner${rounded}`} style={{ whiteSpace: "pre-wrap", pointerEvents: ((this.Document.isButton || this.props.onClick) && !this.props.isSelected()) ? "none" : undefined }} ref={this.createDropTarget} /> + </div> + {this.props.Document.hideSidebar ? (null) : this.sidebarWidthPercent === "0%" ? + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> : + <div className={"formattedTextBox-sidebar" + (InkingControl.Instance.selectedTool !== InkTool.None ? "-inking" : "")} + style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${StrCast(this.extensionDoc?.backgroundColor, "transparent")}` }}> + <CollectionFreeFormView {...this.props} + PanelHeight={this.props.PanelHeight} + PanelWidth={() => this.sidebarWidth} + annotationsKey={this.annotationsKey} + isAnnotationOverlay={false} + focus={this.props.focus} + isSelected={this.props.isSelected} + select={emptyFunction} + active={this.annotationsActive} + ContentScaling={returnOne} + whenActiveChanged={this.whenActiveChanged} + removeDocument={this.removeDocument} + moveDocument={this.moveDocument} + addDocument={(doc: Doc) => { doc.hideSidebar = true; return this.addDocument(doc); }} + CollectionView={undefined} + ScreenToLocalTransform={() => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth), 0)} + ruleProvider={undefined} + renderDepth={this.props.renderDepth + 1} + ContainingCollectionDoc={this.props.ContainingCollectionDoc} + chromeCollapsed={true}> + </CollectionFreeFormView> + <div className="formattedTextBox-sidebar-handle" onPointerDown={this.sidebarDown} onClick={e => this.toggleSidebar()} /> + </div>} <div className="formattedTextBox-dictation" onClick={e => { this._recording ? this.stopDictation(true) : this.recordDictation(); @@ -1059,7 +1203,7 @@ export class FormattedTextBox extends DocExtendableComponent<(FieldViewProps & F e.stopPropagation(); }} > <FontAwesomeIcon className="formattedTExtBox-audioFont" - style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.2 }} icon={"microphone"} size="sm" /> + style={{ color: this._recording ? "red" : "blue", opacity: this._recording ? 1 : 0.5, display: this.props.isSelected() ? "" : "none" }} icon={"microphone"} size="sm" /> </div> </div> ); |