aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/FormattedTextBox.tsx
diff options
context:
space:
mode:
authorStanley Yip <stanley_yip@brown.edu>2019-10-13 15:58:23 -0400
committerStanley Yip <stanley_yip@brown.edu>2019-10-13 15:58:23 -0400
commitb50381f4872d4e5eaa6d1d8949ea4e10d468cfd8 (patch)
treec2939d230a5618aa609e4db84bfdee923bad80bd /src/client/views/nodes/FormattedTextBox.tsx
parentbd796f24677b2aacd32a371da6d2b7c7eb354018 (diff)
parentbaf6ed901d341cade58741d363bbc475519558ae (diff)
Merge branch 'master' of https://github.com/browngraphicslab/Dash-Web into interaction_stanley
Diffstat (limited to 'src/client/views/nodes/FormattedTextBox.tsx')
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx486
1 files changed, 252 insertions, 234 deletions
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 749886d9a..b05d0046c 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -1,53 +1,54 @@
import { library } from '@fortawesome/fontawesome-svg-core';
import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import _ from "lodash";
import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
+import { inputRules } from 'prosemirror-inputrules';
import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
+import { Fragment, Mark, Node, Node as ProsNode, Slice } from "prosemirror-model";
+import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from "prosemirror-state";
+import { ReplaceStep } from 'prosemirror-transform';
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../new_fields/DateField';
-import { Doc, DocListCast, Opt, WidthSym, DocListCastAsync } from "../../../new_fields/Doc";
+import { Doc, DocListCastAsync, Opt, WidthSym, HeightSym } from "../../../new_fields/Doc";
import { Copy, Id } from '../../../new_fields/FieldSymbols';
-import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
-import { BoolCast, Cast, NumCast, StrCast, DateCast, PromiseValue } from "../../../new_fields/Types";
+import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils, numberRange, timenow } from '../../../Utils';
+import { Cast, DateCast, NumCast, StrCast } from "../../../new_fields/Types";
+import { numberRange, Utils, addStyleSheet, addStyleSheetRule, clearStyleSheetRules } 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 { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
+import { FootnoteView, ImageResizeView, DashDocView, OrderedListView, schema, SummarizedView } 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 { DocComponent } from "../DocComponent";
+import { DocumentButtonBar } from '../DocumentButtonBar';
+import { DocumentDecorations } from '../DocumentDecorations';
import { InkingControl } from "../InkingControl";
import { FieldView, FieldViewProps } from "./FieldView";
import "./FormattedTextBox.scss";
+import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
-import { GoogleApiClientUtils, Pulls, Pushes } from '../../apis/google_docs/GoogleApiClientUtils';
-import { DocumentDecorations } from '../DocumentDecorations';
-import { DictationManager } from '../../util/DictationManager';
-import { ReplaceStep } from 'prosemirror-transform';
-import { DocumentType } from '../../documents/DocumentTypes';
-import { RichTextUtils } from '../../../new_fields/RichTextUtils';
-import _ from "lodash";
-import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
-import { inputRules } from 'prosemirror-inputrules';
-import { DocumentButtonBar } from '../DocumentButtonBar';
+import { ContextMenuProps } from '../ContextMenuItem';
+import { ContextMenu } from '../ContextMenu';
+import { TextShadowProperty } from 'csstype';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
export interface FormattedTextBoxProps {
- isOverlay?: boolean;
hideOnLeave?: boolean;
height?: string;
color?: string;
@@ -78,8 +79,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
private _applyingChange: boolean = false;
- private _linkClicked = "";
private _nodeClicked: any;
+ private _searchIndex = 0;
private _undoTyping?: UndoManager.Batch;
private _searchReactionDisposer?: Lambda;
private _scrollToRegionReactionDisposer: Opt<IReactionDisposer>;
@@ -96,9 +97,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@observable private _fontFamily = "Arial";
@observable private _fontAlign = "";
@observable private _entered = false;
- @observable public static InputBoxOverlay?: FormattedTextBox = undefined;
public static SelectOnLoad = "";
- public static InputBoxOverlayScroll: number = 0;
public static IsFragment(html: string) {
return html.indexOf("data-pm-slice") !== -1;
}
@@ -120,8 +119,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return "";
}
- public static getToolTip() {
- return this._toolTipTextMenu;
+ public static getToolTip(ev: EditorView) {
+ return this._toolTipTextMenu ? this._toolTipTextMenu : this._toolTipTextMenu = new TooltipTextMenu(ev);
}
@undoBatch
@@ -138,55 +137,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
constructor(props: FieldViewProps) {
super(props);
- if (this.props.isOverlay) {
- DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
- }
FormattedTextBox.Instance = this;
- this._scrollToRegionReactionDisposer = reaction(
- () => StrCast(this.props.Document.scrollToLinkID),
- async (scrollToLinkID) => {
- let findLinkFrag = (frag: Fragment, editor: EditorView) => {
- const nodes: Node[] = [];
- frag.forEach((node, index) => {
- let examinedNode = findLinkNode(node, editor);
- if (examinedNode && examinedNode.textContent) {
- nodes.push(examinedNode);
- start += index;
- }
- });
- return { frag: Fragment.fromArray(nodes), start: start };
- };
- let findLinkNode = (node: Node, editor: EditorView) => {
- if (!node.isText) {
- const content = findLinkFrag(node.content, editor);
- return node.copy(content.frag);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
- return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
- };
-
- let start = -1;
- if (this._editorView && scrollToLinkID) {
- let editor = this._editorView;
- let ret = findLinkFrag(editor.state.doc.content, editor);
-
- if (ret.frag.size > 2 && ((!this.props.isOverlay && !this.props.isSelected()) || (this.props.isSelected() && this.props.isOverlay))) {
- let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
- if (ret.frag.firstChild) {
- selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
- }
- editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
- const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
- setTimeout(() => this.unhighlightSearchTerms(), 2000);
- }
- this.props.Document.scrollToLinkID = undefined;
- }
-
- },
- { fireImmediately: true }
- );
}
public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
@@ -195,29 +146,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@computed get dataDoc() { return this.props.DataDoc && this.props.Document.isTemplate ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
- // this should be internal to prosemirror, but is needed
- // here to make sure that footnote view nodes in the overlay editor
- // get removed when they're not selected.
-
- syncNodeSelection(view: any, sel: any) {
- if (sel instanceof NodeSelection) {
- var desc = view.docView.descAt(sel.from);
- if (desc !== view.lastSelectedViewDesc) {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- if (desc) { desc.selectNode(); }
- view.lastSelectedViewDesc = desc;
- }
- } else {
- if (view.lastSelectedViewDesc) {
- view.lastSelectedViewDesc.deselectNode();
- view.lastSelectedViewDesc = null;
- }
- }
- }
-
linkOnDeselect: Map<string, string> = new Map();
doLinkOnDeselect() {
@@ -230,7 +158,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, width: 500, height: 500 }, value);
DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id, true);
+ else DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: this.dataDoc[key] as Doc }, "Ref:" + value, "link to named target", id);
});
});
});
@@ -263,7 +191,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
- this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
@@ -286,44 +213,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- public highlightSearchTerms = (terms: String[]) => {
+ public highlightSearchTerms = (terms: string[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- let nodeText: String = node.text;
- let tokens = nodeText.split(" ");
- let start = pos;
- tokens.forEach((word) => {
- if (terms.includes(word) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.addMark(start, start + word.length, mark).removeStoredMark(mark));
- }
- start += word.length + 1;
- });
- }
- });
+ 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));
+ let tr = this._editorView.state.tr;
+ let flattened: TextSelection[] = [];
+ res.map(r => r.map(h => flattened.push(h)));
+ let 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());
}
}
public unhighlightSearchTerms = () => {
if (this._editorView && (this._editorView as any).docView) {
- const doc = this._editorView.state.doc;
const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- doc.nodesBetween(0, doc.content.size, (node: ProsNode, pos: number, parent: ProsNode, index: number) => {
- if (node.isLeaf && node.isText && node.text) {
- if (node.marks.includes(mark) && this._editorView) {
- this._editorView.dispatch(this._editorView.state.tr.removeMark(pos, pos + node.nodeSize, mark));
- }
- }
- });
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ let 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 mid = view.state.doc.resolve(Math.round((start + end) / 2));
let nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: keep ? Doc.CurrentUserEmail : mark.attrs.userid, opened: opened });
- view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark).setSelection(new TextSelection(mid)));
+ view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
}
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
@@ -335,14 +251,19 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@action
drop = async (e: Event, de: DragManager.DropEvent) => {
// We're dealing with a link to a document
- if (de.data instanceof DragManager.EmbedDragData && de.data.urlField) {
+ if (de.data instanceof DragManager.EmbedDragData) {
let target = de.data.embeddableSourceDoc;
// We're dealing with an internal document drop
- let url = de.data.urlField.url.href;
- let model: NodeType = [".mov", ".mp4"].includes(url) ? schema.nodes.video : schema.nodes.image;
+ const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+ let alias = Doc.MakeAlias(target);
+ alias.fitToBox = true;
+ let node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: alias[Id],
+ float: "right"
+ });
let pos = this._editorView!.posAtCoords({ left: de.x, top: de.y });
- this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, model.create({ src: url, docid: target[Id] })));
- DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "ImgRef:" + target.title);
+ link && this._editorView!.dispatch(this._editorView!.state.tr.insert(pos!.pos, node));
this.tryUpdateHeight();
e.stopPropagation();
} else if (de.data instanceof DragManager.DocumentDragData) {
@@ -353,7 +274,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data);
e.stopPropagation();
}
- } else {
+ } else if (de.mods === "CtrlKey") {
draggedDoc.isTemplate = true;
if (typeof (draggedDoc.layout) === "string") {
let layoutDelegateToOverrideFieldKey = Doc.MakeDelegate(draggedDoc);
@@ -368,16 +289,99 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- recordKeyHandler = (e: KeyboardEvent) => {
- if (SelectionManager.SelectedDocuments().length && this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
- if (e.key === "R" && e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- this.recordBullet();
+ getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+ let offset = 0;
+
+ if (context === node) return { from: offset, to: offset + node.nodeSize };
+
+ if (node.isBlock) {
+ for (let i = 0; i < (context.content as any).content.length; i++) {
+ let result = this.getNodeEndpoints((context.content as any).content[i], node);
+ if (result) {
+ return {
+ from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
+ to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+ };
+ }
+ offset += (context.content as any).content[i].nodeSize;
}
+ return null;
+ } else {
+ return null;
}
}
+
+ //Recursively finds matches within a given node
+ findInNode(pm: EditorView, node: Node, find: string) {
+ let ret: TextSelection[] = [];
+
+ if (node.isTextblock) {
+ let index = 0, foundAt, 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));
+ ret.push(sel);
+ index = index + foundAt + find.length;
+ }
+ } else {
+ node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
+ }
+ return ret;
+ }
+ static _highlights: string[] = [];
+
+ updateHighlights = () => {
+ clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+ if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-remote", { background: "yellow" });
+ }
+ if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "0" });
+ }
+ 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);
+ 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);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "userMark-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ }
+ }
+
+ specificContextMenu = (e: React.MouseEvent): void => {
+ let funcs: ContextMenuProps[] = [];
+ funcs.push({ description: "Dictate", 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({
+ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+ e.stopPropagation();
+ if (FormattedTextBox._highlights.indexOf(option) === -1) {
+ FormattedTextBox._highlights.push(option);
+ } else {
+ FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ }
+ this.updateHighlights();
+ }, icon: "expand-arrows-alt"
+ }));
+
+ ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: funcs, icon: "asterisk" });
+ }
+
recordBullet = async () => {
let completedCue = "end session";
let results = await DictationManager.Controls.listen({
@@ -408,12 +412,14 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
nextBullet = (pos: number) => {
if (this._editorView) {
let frag = Fragment.fromArray(this.newListItems(2));
- let slice = new Slice(frag, 2, 2);
- let state = this._editorView.state;
- this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
- pos += 4;
- state = this._editorView.state;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ if (this._editorView.state.doc.resolve(pos).depth >= 2) {
+ let slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
}
}
@@ -424,9 +430,10 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
_keymap: any = undefined;
@computed get config() {
this._keymap = buildKeymap(schema);
+ (schema as any).Document = this.props.Document;
return {
schema,
- plugins: this.props.isOverlay ? [
+ plugins: [
inputRules(inpRules),
this.tooltipTextMenuPlugin(),
history(),
@@ -439,26 +446,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}),
formattedTextBoxCommentPlugin
- ] : [
- history(),
- keymap(this._keymap),
- keymap(baseKeymap),
- ]
+ ]
};
}
componentDidMount() {
-
- if (!this.props.isOverlay) {
- this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
- () => {
- if (this.props.isSelected()) {
- FormattedTextBox.InputBoxOverlay = this;
- FormattedTextBox.InputBoxOverlayScroll = this._ref.current!.scrollTop;
- }
- }, { fireImmediately: true });
- }
-
this.pullFromGoogleDoc(this.checkState);
this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => DocumentDecorations.Instance.isAnimatingFetch = true);
@@ -519,11 +511,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.search_string);
}, searchString => {
- const fieldkey = 'preview';
- let preview = false;
- // if (!this._editorView && Object.keys(this.props.Document).indexOf(fieldkey) !== -1) {
- // preview = true;
- // }
if (searchString) {
this.highlightSearchTerms([searchString]);
}
@@ -547,7 +534,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
},
action((rules: any) => {
this._fontFamily = rules ? rules.font : "Arial";
- this._fontSize = rules ? rules.size : 13;
+ this._fontSize = rules ? rules.size : NumCast(this.props.Document.fontSize, 13);
rules && setTimeout(() => {
const view = this._editorView!;
if (this._proseRef) {
@@ -563,6 +550,51 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}, 0);
}), { fireImmediately: true }
);
+ this._scrollToRegionReactionDisposer = reaction(
+ () => StrCast(this.props.Document.scrollToLinkID),
+ async (scrollToLinkID) => {
+ let findLinkFrag = (frag: Fragment, editor: EditorView) => {
+ const nodes: Node[] = [];
+ frag.forEach((node, index) => {
+ let examinedNode = findLinkNode(node, editor);
+ if (examinedNode && examinedNode.textContent) {
+ nodes.push(examinedNode);
+ start += index;
+ }
+ });
+ return { frag: Fragment.fromArray(nodes), start: start };
+ };
+ let findLinkNode = (node: Node, editor: EditorView) => {
+ if (!node.isText) {
+ const content = findLinkFrag(node.content, editor);
+ return node.copy(content.frag);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+ return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+ };
+
+ let start = -1;
+ if (this._editorView && scrollToLinkID) {
+ let editor = this._editorView;
+ let 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
+ if (ret.frag.firstChild) {
+ selection = TextSelection.between(editor.state.doc.resolve(ret.start + 2), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+ }
+ editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+ setTimeout(() => this.unhighlightSearchTerms(), 2000);
+ }
+ this.props.Document.scrollToLinkID = undefined;
+ }
+
+ },
+ { fireImmediately: true }
+ );
setTimeout(() => this.tryUpdateHeight(), 0);
}
@@ -741,6 +773,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
},
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
+ 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); },
ordered_list(node, view, getPos) { return new OrderedListView(); },
@@ -749,7 +782,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));
@@ -761,9 +793,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
FormattedTextBox.SelectOnLoad = "";
this.props.select(false);
}
- else if (this.props.isOverlay) this._editorView!.focus();
+ 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: timenow() })];
+ 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) })];
}
getFont(font: string) {
switch (font) {
@@ -790,8 +822,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._searchReactionDisposer && this._searchReactionDisposer();
this._editorView && this._editorView.destroy();
}
-
-
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));
@@ -801,31 +831,25 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.button === 0 && this.props.isSelected() && !e.altKey && !e.ctrlKey && !e.metaKey) {
e.stopPropagation();
}
- let ctrlKey = e.ctrlKey;
if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
e.preventDefault();
}
}
onPointerUp = (e: React.PointerEvent): void => {
- FormattedTextBoxComment.textBox = this;
+ if (!(e.nativeEvent as any).formattedHandled) { FormattedTextBoxComment.textBox = this; }
+ (e.nativeEvent as any).formattedHandled = true;
+
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ static InputBoxOverlay: FormattedTextBox | undefined;
@action
onFocused = (e: React.FocusEvent): void => {
- document.removeEventListener("keypress", this.recordKeyHandler);
- document.addEventListener("keypress", this.recordKeyHandler);
+ FormattedTextBox.InputBoxOverlay = this;
this.tryUpdateHeight();
- if (!this.props.isOverlay) {
- FormattedTextBox.InputBoxOverlay = this;
- } else {
- if (this._ref.current) {
- this._ref.current.scrollTop = FormattedTextBox.InputBoxOverlayScroll;
- }
- }
}
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
@@ -834,17 +858,18 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ static _bulletStyleSheet: any = addStyleSheet();
+ static _userStyleSheet: any = addStyleSheet();
+
onClick = (e: React.MouseEvent): void => {
- let ctrlKey = e.ctrlKey;
+ 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;
}
- for (let parent = (e.target as any).parentNode; !href && parent; parent = parent.parentNode) {
- href = parent.childNodes[0].href ? parent.childNodes[0].href : parent.href;
- }
let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
if (node) {
@@ -856,57 +881,42 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
if (href) {
if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- this._linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- if (this._linkClicked) {
- DocServer.GetRefField(this._linkClicked).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- let proto = Doc.GetProto(linkDoc);
- let targetContext = await Cast(proto.targetContext, Doc);
- let jumpToDoc = await Cast(linkDoc.anchor2, Doc);
-
- if (jumpToDoc) {
- if (DocumentManager.Instance.getDocumentView(jumpToDoc)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, e.altKey);
- return;
- }
- }
- if (targetContext && (!jumpToDoc || targetContext !== await jumpToDoc.annotationOn)) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc || targetContext, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"), targetContext);
- } else if (jumpToDoc) {
- DocumentManager.Instance.jumpToDocument(jumpToDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- } else {
- DocumentManager.Instance.jumpToDocument(linkDoc, ctrlKey, document => this.props.addDocTab(document, undefined, location ? location : "inTab"));
- }
- }
+ 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);
});
- e.stopPropagation();
- e.preventDefault();
}
} else {
let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.props.Document.x, 0) + NumCast(this.props.Document.width, 0), y: NumCast(this.props.Document.y) });
this.props.addDocument && this.props.addDocument(webDoc);
- this._linkClicked = webDoc[Id];
}
e.stopPropagation();
e.preventDefault();
}
-
}
- // 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.
- if (this.props.isSelected() && e.nativeEvent.offsetX < 40) {
- let pos = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+
+ this.hitBulletTargets(e.clientX, e.clientY, e.nativeEvent.offsetX, e.shiftKey);
+ }
+
+ // 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) {
+ 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 = beforeEle ? window.getComputedStyle(beforeEle, ':before') : undefined;
+ 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 && e.nativeEvent.offsetX < beforeWidth) {
+ 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 && !e.shiftKey) {
+ 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 }));
}
@@ -914,25 +924,28 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
}
- this._proseRef!.focus();
- if (this._linkClicked) {
- this._linkClicked = "";
- e.preventDefault();
- e.stopPropagation();
- }
}
- onMouseDown = (e: React.MouseEvent): void => {
- if (!this.props.isSelected()) { // preventing default allows the onClick to be generated instead of being swallowed by the text box itself
- e.preventDefault(); // bcz: this would normally be in OnPointerDown - however, if done there, no mouse move events will be generated which makes transititioning to GoldenLayout's drag interactions impossible
+ onMouseUp = (e: React.MouseEvent): void => {
+ e.stopPropagation();
+
+ // 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 ((this._editorView as any).mouseDown) {
+ let originalUpHandler = (this._editorView as any).mouseDown.up;
+ (this._editorView as any).root.removeEventListener("mouseup", originalUpHandler);
+ (this._editorView as any).mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ (this._editorView as any).root.addEventListener("mouseup", (this._editorView as any).mouseDown.up);
}
}
tooltipTextMenuPlugin() {
- let myprops = this.props;
let self = FormattedTextBox;
return new Plugin({
- view(_editorView) {
- return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
+ view(newView) {
+ return self._toolTipTextMenu = FormattedTextBox.getToolTip(newView);
}
});
}
@@ -946,7 +959,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
});
}
onBlur = (e: any) => {
- document.removeEventListener("keypress", this.recordKeyHandler);
+ DictationManager.Controls.stop(false);
if (this._undoTyping) {
this._undoTyping.end();
this._undoTyping = undefined;
@@ -961,7 +974,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
if (e.key === "Tab" || e.key === "Enter") {
e.preventDefault();
}
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })));
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.round(Date.now() / 1000 / 5) });
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
if (!this._undoTyping) {
this._undoTyping = UndoManager.StartBatch("undoTyping");
@@ -972,7 +986,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
tryUpdateHeight() {
const ChromeHeight = this.props.ChromeHeight;
let sh = this._ref.current ? this._ref.current.scrollHeight : 0;
- if (!this.props.isOverlay && !this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0) {
+ if (!this.props.Document.isAnimating && this.props.Document.autoHeight && sh !== 0 && getComputedStyle(this._ref.current!.parentElement!).top === "0px") {
let nh = this.props.Document.isTemplate ? 0 : NumCast(this.dataDoc.nativeHeight, 0);
let dh = NumCast(this.props.Document.height, 0);
this.props.Document.height = Math.max(10, (nh ? dh / nh * sh : sh) + (ChromeHeight ? ChromeHeight() : 0));
@@ -981,30 +995,34 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
render() {
- let style = this.props.isOverlay ? "scroll" : "hidden";
+ let style = "hidden";
let rounded = StrCast(this.props.Document.borderRounding) === "100%" ? "-rounded" : "";
let interactive: "all" | "none" = InkingControl.Instance.selectedTool || this.props.Document.isBackground
? "none" : "all";
Doc.UpdateDocumentExtensionForField(this.dataDoc, this.props.fieldKey);
+ if (this.props.isSelected()) {
+ FormattedTextBox._toolTipTextMenu!.updateFromDash(this._editorView!, undefined, this.props);
+ }
return (
<div className={`formattedTextBox-cont-${style}`} ref={this._ref}
style={{
overflowY: this.props.Document.autoHeight ? "hidden" : "auto",
height: this.props.Document.autoHeight ? "max-content" : this.props.height ? this.props.height : undefined,
background: this.props.hideOnLeave ? "rgba(0,0,0 ,0.4)" : undefined,
- opacity: this.props.hideOnLeave ? (this._entered || this.props.isSelected() || Doc.IsBrushed(this.props.Document) ? 1 : 0.1) : 1,
+ 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,
}}
+ onContextMenu={this.specificContextMenu}
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
- onMouseDown={this.onMouseDown}
+ onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
onPointerLeave={action(() => this._entered = false)}