aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/FormattedTextBox.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/FormattedTextBox.tsx')
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx624
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>
);