aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/FormattedTextBox.tsx
diff options
context:
space:
mode:
authorbob <bcz@cs.brown.edu>2019-09-03 16:47:52 -0400
committerbob <bcz@cs.brown.edu>2019-09-03 16:47:52 -0400
commit25e8cc61f67bed3063dc81997d31f29e451b5610 (patch)
tree3381aa45ceddd72068385d260473403f30fb693f /src/client/views/nodes/FormattedTextBox.tsx
parent7e87aa4b7e0125482c87ab61f4a0de14e774558d (diff)
parenta4d36f835b5c43351d1761034b61513b000445ba (diff)
merged with master
Diffstat (limited to 'src/client/views/nodes/FormattedTextBox.tsx')
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx203
1 files changed, 118 insertions, 85 deletions
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6af5c8f0b..b671d06ea 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -5,8 +5,8 @@ import { observer } from "mobx-react";
import { baseKeymap } from "prosemirror-commands";
import { history } from "prosemirror-history";
import { keymap } from "prosemirror-keymap";
-import { Fragment, Node, Node as ProsNode, NodeType, Slice } from "prosemirror-model";
-import { EditorState, Plugin, Transaction, TextSelection } from "prosemirror-state";
+import { Fragment, Node, Node as ProsNode, NodeType, Slice, Mark, ResolvedPos } from "prosemirror-model";
+import { EditorState, Plugin, Transaction, TextSelection, NodeSelection } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import { DateField } from '../../../new_fields/DateField';
import { Doc, DocListCast, Opt, WidthSym } from "../../../new_fields/Doc";
@@ -15,14 +15,14 @@ import { List } from '../../../new_fields/List';
import { RichTextField } from "../../../new_fields/RichTextField";
import { BoolCast, Cast, NumCast, StrCast, DateCast } from "../../../new_fields/Types";
import { createSchema, makeInterface } from "../../../new_fields/Schema";
-import { Utils } from '../../../Utils';
+import { Utils, numberRange } from '../../../Utils';
import { DocServer } from "../../DocServer";
import { Docs, DocUtils } from '../../documents/Documents';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -39,6 +39,7 @@ import { ReplaceStep } from 'prosemirror-transform';
import { DocumentType } from '../../documents/DocumentTypes';
import { RichTextUtils } from '../../../new_fields/RichTextUtils';
import * as _ from "lodash";
+import { formattedTextBoxCommentPlugin, FormattedTextBoxComment } from './FormattedTextBoxComment';
library.add(faEdit);
library.add(faSmile, faTextHeight, faUpload);
@@ -68,27 +69,27 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
public static LayoutString(fieldStr: string = "data") {
return FieldView.LayoutString(FormattedTextBox, fieldStr);
}
- public static blankState = () => EditorState.create(FormattedTextBox.Instance._configuration);
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
public static Instance: FormattedTextBox;
- private _configuration: any;
- private _ref: React.RefObject<HTMLDivElement>;
+ private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
+ private _ref: React.RefObject<HTMLDivElement> = React.createRef();
private _proseRef?: HTMLDivElement;
private _editorView: Opt<EditorView>;
- private static _toolTipTextMenu: TooltipTextMenu | undefined = undefined;
private _applyingChange: boolean = false;
private _linkClicked = "";
+ private _undoTyping?: UndoManager.Batch;
private _reactionDisposer: Opt<IReactionDisposer>;
private _searchReactionDisposer?: Lambda;
private _textReactionDisposer: Opt<IReactionDisposer>;
private _heightReactionDisposer: Opt<IReactionDisposer>;
private _proxyReactionDisposer: Opt<IReactionDisposer>;
- private pullReactionDisposer: Opt<IReactionDisposer>;
- private pushReactionDisposer: Opt<IReactionDisposer>;
+ private _pullReactionDisposer: Opt<IReactionDisposer>;
+ private _pushReactionDisposer: Opt<IReactionDisposer>;
private dropDisposer?: DragManager.DragDropDisposer;
- public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
- @observable _entered = false;
+ @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;
@@ -117,7 +118,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
@undoBatch
public setFontColor(color: string) {
- let self = this;
if (this._editorView!.state.selection.from === this._editorView!.state.selection.to) return false;
if (this._editorView!.state.selection.to - this._editorView!.state.selection.from > this._editorView!.state.doc.nodeSize - 3) {
this.props.Document.color = color;
@@ -130,15 +130,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
constructor(props: FieldViewProps) {
super(props);
- FormattedTextBox.Instance = this;
- this._ref = React.createRef();
if (this.props.isOverlay) {
DragManager.StartDragFunctions.push(() => FormattedTextBox.InputBoxOverlay = undefined);
}
-
- document.addEventListener("paste", this.paste);
}
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
@computed get extensionDoc() { return Doc.resolvedFieldDataDoc(this.dataDoc, this.props.fieldKey, "dummy"); }
@computed get dataDoc() { return this.props.DataDoc && (BoolCast(this.props.Document.isTemplate) || BoolCast(this.props.DataDoc.isTemplate) || this.props.DataDoc.layout === this.props.Document) ? this.props.DataDoc : Doc.GetProto(this.props.Document); }
@@ -172,21 +170,40 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ // 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;
+ }
+ }
+ }
+
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = true);
this._editorView.updateState(state);
- FormattedTextBox._toolTipTextMenu && (FormattedTextBox._toolTipTextMenu.HackToFixTextSelectionGlitch = false);
- if (state.selection.empty && FormattedTextBox._toolTipTextMenu) {
- const marks = tx.storedMarks;
- if (marks) { FormattedTextBox._toolTipTextMenu.mark_key_pressed(marks); }
+ 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);
}
this._applyingChange = true;
- const fieldkey = "preview";
- if (this.extensionDoc) this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n");
- if (this.extensionDoc) this.extensionDoc.lastModified = new DateField(new Date(Date.now()));
+ this.extensionDoc && (this.extensionDoc.text = state.doc.textBetween(0, state.doc.content.size, "\n\n"));
+ this.extensionDoc && (this.extensionDoc.lastModified = new DateField(new Date(Date.now())));
this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()));
this._applyingChange = false;
let title = StrCast(this.dataDoc.title);
@@ -200,7 +217,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
public highlightSearchTerms = (terms: String[]) => {
if (this._editorView && (this._editorView as any).docView) {
- const fieldkey = "preview";
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) => {
@@ -241,12 +257,8 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
protected createDropTarget = (ele: HTMLDivElement) => {
this._proseRef = ele;
- if (this.dropDisposer) {
- this.dropDisposer();
- }
- if (ele) {
- this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } });
- }
+ this.dropDisposer && this.dropDisposer();
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, { handlers: { drop: this.drop.bind(this) } }));
}
@undoBatch
@@ -270,13 +282,12 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
recordKeyHandler = (e: KeyboardEvent) => {
- if (this.props.Document !== SelectionManager.SelectedDocuments()[0].props.Document) {
- return;
- }
- if (e.key === "R" && e.altKey) {
- e.stopPropagation();
- e.preventDefault();
- this.recordBullet();
+ if (this.props.Document === SelectionManager.SelectedDocuments()[0].props.Document) {
+ if (e.key === "R" && e.altKey) {
+ e.stopPropagation();
+ e.preventDefault();
+ this.recordBullet();
+ }
}
}
@@ -320,15 +331,11 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
private newListItems = (count: number) => {
- let listItems: any[] = [];
- for (let i = 0; i < count; i++) {
- listItems.push(schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
- }
- return listItems;
+ return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
}
- componentDidMount() {
- this._configuration = {
+ @computed get config() {
+ return {
schema,
inpRules, //these currently don't do anything, but could eventually be helpful
plugins: this.props.isOverlay ? [
@@ -341,13 +348,23 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
props: {
attributes: { class: "ProseMirror-example-setup-style" }
}
- })
+ }),
+ formattedTextBoxCommentPlugin
] : [
history(),
keymap(buildKeymap(schema)),
keymap(baseKeymap),
]
};
+ }
+
+ @action
+ rebuildEditor() {
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
+ }
+
+ componentDidMount() {
+ document.addEventListener("paste", this.paste);
if (!this.props.isOverlay) {
this._proxyReactionDisposer = reaction(() => this.props.isSelected(),
@@ -370,13 +387,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
incomingValue => {
if (this._editorView && !this._applyingChange) {
let updatedState = JSON.parse(incomingValue);
- this._editorView.updateState(EditorState.fromJSON(this._configuration, updatedState));
+ this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
this.tryUpdateHeight();
}
}
);
- this.pullReactionDisposer = reaction(
+ this._pullReactionDisposer = reaction(
() => this.props.Document[Pulls],
() => {
if (!DocumentDecorations.hasPulledHack) {
@@ -387,7 +404,7 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
);
- this.pushReactionDisposer = reaction(
+ this._pushReactionDisposer = reaction(
() => this.props.Document[Pushes],
() => {
if (!DocumentDecorations.hasPushedHack) {
@@ -412,7 +429,9 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this.dataDoc.lastModified = undefined;
}
}, { fireImmediately: true });
- this.setupEditor(this._configuration, this.dataDoc, this.props.fieldKey);
+
+
+ this.setupEditor(this.config, this.dataDoc, this.props.fieldKey);
this._searchReactionDisposer = reaction(() => {
return StrCast(this.props.Document.search_string);
@@ -509,7 +528,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
-
clipboardTextSerializer = (slice: Slice): string => {
let text = "", separated = true;
const from = 0, to = slice.content.size;
@@ -609,37 +627,47 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
if (this._proseRef) {
+ this._editorView && this._editorView.destroy();
this._editorView = new EditorView(this._proseRef, {
state: field && field.Data ? EditorState.fromJSON(config, JSON.parse(field.Data)) : EditorState.create(config),
dispatchTransaction: this.dispatchTransaction,
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
+ ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); },
+ footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) }
},
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));
}
}
- if (this.props.selectOnLoad) {
- if (!this.props.isOverlay) this.props.select(false);
- else this._editorView!.focus();
+ if (this.props.Document[Id] === FormattedTextBox.SelectOnLoad) {
+ FormattedTextBox.SelectOnLoad = "";
+ this.props.select(false);
}
+ else if (this.props.isOverlay) this._editorView!.focus();
+ var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
+ let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail })];
+ this._editorView!.state.storedMarks = newMarks;
+
}
componentWillUnmount() {
- this._editorView && this._editorView.destroy();
this._reactionDisposer && this._reactionDisposer();
this._proxyReactionDisposer && this._proxyReactionDisposer();
this._textReactionDisposer && this._textReactionDisposer();
- this.pushReactionDisposer && this.pushReactionDisposer();
- this.pullReactionDisposer && this.pullReactionDisposer();
+ this._pushReactionDisposer && this._pushReactionDisposer();
+ this._pullReactionDisposer && this._pullReactionDisposer();
this._heightReactionDisposer && this._heightReactionDisposer();
this._searchReactionDisposer && this._searchReactionDisposer();
+ document.removeEventListener("paste", this.paste);
+ this._editorView && this._editorView.destroy();
}
onPointerDown = (e: React.PointerEvent): void => {
@@ -703,15 +731,21 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
e.preventDefault();
}
}
+
onPointerUp = (e: React.PointerEvent): void => {
- if (FormattedTextBox._toolTipTextMenu && FormattedTextBox._toolTipTextMenu.tooltip) {
- //this._toolTipTextMenu.tooltip.style.opacity = "1";
- }
+ FormattedTextBoxComment.textBox = this;
if (e.buttons === 1 && this.props.isSelected() && !e.altKey) {
e.stopPropagation();
}
}
+ 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, mid)));
+ }
+
@action
onFocused = (e: React.FocusEvent): void => {
document.removeEventListener("keypress", this.recordKeyHandler);
@@ -754,7 +788,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
return self._toolTipTextMenu = new TooltipTextMenu(_editorView, myprops);
}
});
- //this.props.Document.tooltip = self._toolTipTextMenu;
}
tooltipLinkingMenuPlugin() {
@@ -772,13 +805,32 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
this._undoTyping = undefined;
}
}
- public _undoTyping?: UndoManager.Batch;
onKeyPress = (e: React.KeyboardEvent) => {
if (e.key === "Escape") {
SelectionManager.DeselectAll();
}
e.stopPropagation();
- if (e.key === "Tab") e.preventDefault();
+ if (e.key === "Tab" || e.key === "Enter") { // bullets typically change "levels" when tab or enter is used. sometimes backspcae, so maybe that should be added.
+ e.preventDefault();
+ }
+ function timenow() {
+ var now = new Date();
+ let ampm = 'am';
+ let h = now.getHours();
+ let m: any = now.getMinutes();
+ let s: any = now.getSeconds();
+ if (h >= 12) {
+ if (h > 12) h -= 12;
+ ampm = 'pm';
+ }
+
+ if (m < 10) m = '0' + m;
+ return now.toLocaleDateString() + ' ' + h + ':' + m + ' ' + ampm;
+ }
+ var markerss = this._editorView!.state.storedMarks || (this._editorView!.state.selection.$to.parentOffset && this._editorView!.state.selection.$from.marks());
+ let newMarks = [...(markerss ? markerss.filter(m => m.type !== schema.marks.user_mark) : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: timenow() })];
+ this._editorView!.state.storedMarks = newMarks;
+
// stop propagation doesn't seem to stop propagation of native keyboard events.
// so we set a flag on the native event that marks that the event's been handled.
(e.nativeEvent as any).DASHFormattedTextBoxHandled = true;
@@ -805,24 +857,6 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
- @action
- onPointerEnter = (e: React.PointerEvent) => {
- this._entered = true;
- }
- @action
- onPointerLeave = (e: React.PointerEvent) => {
- this._entered = false;
- }
-
- specificContextMenu = (e: React.MouseEvent): void => {
- // let subitems: ContextMenuProps[] = [];
- // subitems.push({
- // description: BoolCast(this.props.Document.autoHeight) ? "Manual Height" : "Auto Height",
- // event: action(() => Doc.GetProto(this.props.Document).autoHeight = !BoolCast(this.props.Document.autoHeight)), icon: "expand-arrows-alt"
- // });
- // ContextMenu.Instance.addItem({ description: "Text Funcs...", subitems: subitems, icon: "text-height" });
- }
-
render() {
let self = this;
@@ -845,14 +879,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
onKeyDown={this.onKeyPress}
onFocus={this.onFocused}
onClick={this.onClick}
- onContextMenu={this.specificContextMenu}
onBlur={this.onBlur}
onPointerUp={this.onPointerUp}
onPointerDown={this.onPointerDown}
onMouseDown={this.onMouseDown}
onWheel={this.onPointerWheel}
- onPointerEnter={this.onPointerEnter}
- onPointerLeave={this.onPointerLeave}
+ onPointerEnter={action(() => this._entered = true)}
+ onPointerLeave={action(() => this._entered = false)}
>
<div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget} style={{ whiteSpace: "pre-wrap" }} />
</div>