aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/DocumentButtonBar.tsx2
-rw-r--r--src/client/views/InkingControl.tsx2
-rw-r--r--src/client/views/MainView.tsx2
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx2
-rw-r--r--src/client/views/nodes/DocumentContentsView.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.tsx17
-rw-r--r--src/client/views/nodes/formattedText/DashDocCommentView.tsx95
-rw-r--r--src/client/views/nodes/formattedText/DashDocView.tsx269
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.scss36
-rw-r--r--src/client/views/nodes/formattedText/DashFieldView.tsx211
-rw-r--r--src/client/views/nodes/formattedText/FootnoteView.tsx162
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.scss (renamed from src/client/views/nodes/FormattedTextBox.scss)2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx (renamed from src/client/views/nodes/FormattedTextBox.tsx)97
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.scss (renamed from src/client/views/nodes/FormattedTextBoxComment.scss)0
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx (renamed from src/client/views/nodes/FormattedTextBoxComment.tsx)20
-rw-r--r--src/client/views/nodes/formattedText/ImageResizeView.tsx138
-rw-r--r--src/client/views/nodes/formattedText/ParagraphNodeSpec.ts143
-rw-r--r--src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts241
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.scss121
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx875
-rw-r--r--src/client/views/nodes/formattedText/RichTextRules.ts319
-rw-r--r--src/client/views/nodes/formattedText/RichTextSchema.tsx718
-rw-r--r--src/client/views/nodes/formattedText/SummaryView.tsx81
-rw-r--r--src/client/views/nodes/formattedText/TooltipTextMenu.scss372
-rw-r--r--src/client/views/nodes/formattedText/marks_rts.ts296
-rw-r--r--src/client/views/nodes/formattedText/nodes_rts.ts264
-rw-r--r--src/client/views/nodes/formattedText/prosemirrorPatches.js139
-rw-r--r--src/client/views/nodes/formattedText/schema_rts.ts26
31 files changed, 4600 insertions, 58 deletions
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index c02f79187..3624cdb6d 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -15,7 +15,7 @@ import './collections/ParentDocumentSelector.scss';
import './DocumentButtonBar.scss';
import { LinkMenu } from "./linking/LinkMenu";
import { DocumentView } from './nodes/DocumentView';
-import { GoogleRef } from "./nodes/FormattedTextBox";
+import { GoogleRef } from "./nodes/formattedText/FormattedTextBox";
import { TemplateMenu } from "./TemplateMenu";
import { Template, Templates } from "./Templates";
import React = require("react");
diff --git a/src/client/views/InkingControl.tsx b/src/client/views/InkingControl.tsx
index 172c1864a..70ea955e1 100644
--- a/src/client/views/InkingControl.tsx
+++ b/src/client/views/InkingControl.tsx
@@ -8,7 +8,7 @@ import { Scripting } from "../util/Scripting";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
import GestureOverlay from "./GestureOverlay";
-import { FormattedTextBox } from "./nodes/FormattedTextBox";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
export class InkingControl {
@observable static Instance: InkingControl;
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 8fb67c435..20238985d 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -19,7 +19,7 @@ import { DocServer } from '../DocServer';
import { Docs, DocumentOptions } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { HistoryUtil } from '../util/History';
-import RichTextMenu from '../util/RichTextMenu';
+import RichTextMenu from './nodes/formattedText/RichTextMenu';
import { Scripting } from '../util/Scripting';
import SettingsManager from '../util/SettingsManager';
import SharingManager from '../util/SharingManager';
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index f4250e96d..eda8e5684 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -11,7 +11,7 @@ import "./CollectionCarouselView.scss";
import { CollectionSubView } from './CollectionSubView';
import { faCaretLeft, faCaretRight } from '@fortawesome/free-solid-svg-icons';
import { Doc } from '../../../new_fields/Doc';
-import { FormattedTextBox } from '../nodes/FormattedTextBox';
+import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { ContextMenu } from '../ContextMenu';
import { ObjectField } from '../../../new_fields/ObjectField';
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 49abc6ee6..fb7535d9f 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -15,7 +15,7 @@ import { DragManager, dropActionType } from "../../util/DragManager";
import { undoBatch, UndoManager } from "../../util/UndoManager";
import { DocComponent } from "../DocComponent";
import { FieldViewProps } from "../nodes/FieldView";
-import { FormattedTextBox, GoogleRef } from "../nodes/FormattedTextBox";
+import { FormattedTextBox, GoogleRef } from "../nodes/formattedText/FormattedTextBox";
import { CollectionView } from "./CollectionView";
import React = require("react");
import { basename } from 'path';
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 28b461313..92b27a0c6 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -32,7 +32,7 @@ import { ContextMenuProps } from "../../ContextMenuItem";
import { InkingControl } from "../../InkingControl";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocumentViewProps, DocumentView } from "../../nodes/DocumentView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { pageSchema } from "../../nodes/ImageBox";
import PDFMenu from "../../pdf/PDFMenu";
import { CollectionDockingView } from "../CollectionDockingView";
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index cd8166309..2d3bb6f3c 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -19,7 +19,7 @@ import React = require("react");
import { CognitiveServices } from "../../../cognitive_services/CognitiveServices";
import { RichTextField } from "../../../../new_fields/RichTextField";
import { CollectionView } from "../CollectionView";
-import { FormattedTextBox } from "../../nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";
import { ScriptField } from "../../../../new_fields/ScriptField";
interface MarqueeViewProps {
diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx
index cd78ac7b3..4d20d3e2c 100644
--- a/src/client/views/nodes/DocumentContentsView.tsx
+++ b/src/client/views/nodes/DocumentContentsView.tsx
@@ -19,7 +19,7 @@ import { DocumentViewProps } from "./DocumentView";
import "./DocumentView.scss";
import { FontIconBox } from "./FontIconBox";
import { FieldView, FieldViewProps } from "./FieldView";
-import { FormattedTextBox } from "./FormattedTextBox";
+import { FormattedTextBox } from "./formattedText/FormattedTextBox";
import { ImageBox } from "./ImageBox";
import { KeyValueBox } from "./KeyValueBox";
import { PDFBox } from "./PDFBox";
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 83eb12436..fdcaa2df3 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -938,9 +938,6 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
this._queries = kps.toString();
}
- onPointerEnter = (e: React.PointerEvent): void => { Doc.BrushDoc(this.props.Document); };
- onPointerLeave = (e: React.PointerEvent): void => { Doc.UnBrushDoc(this.props.Document); };
-
// does Document set a layout prop
// does Document set a layout prop
setsLayoutProp = (prop: string) => this.props.Document[prop] !== this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)] && this.props.Document["default" + prop[0].toUpperCase() + prop.slice(1)];
@@ -1133,7 +1130,19 @@ export class DocumentView extends DocComponent<DocumentViewProps, Document>(Docu
highlighting = highlighting && this.props.focus !== emptyFunction; // bcz: hack to turn off highlighting onsidebar panel documents. need to flag a document as not highlightable in a more direct way
return <div className={`documentView-node${this.topMost ? "-topmost" : ""}`} ref={this._mainCont} onKeyDown={this.onKeyDown}
onContextMenu={this.onContextMenu} onPointerDown={this.onPointerDown} onClick={this.onClick}
- onPointerEnter={e => Doc.BrushDoc(this.props.Document)} onPointerLeave={e => Doc.UnBrushDoc(this.props.Document)}
+ // onPointerEnter={e => Doc.BrushDoc(this.props.Document)}
+ // onPointerLeave={e => Doc.BrushDoc(this.props.Document)}
+ onPointerEnter={action(() => Doc.BrushDoc(this.props.Document))}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ let entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this.ContentDiv) {
+ entered = true;
+ }
+ }
+ !entered && Doc.UnBrushDoc(this.props.Document);
+ })}
style={{
transformOrigin: this._animate ? "center center" : undefined,
transform: this._animate ? `scale(${this._animate})` : undefined,
diff --git a/src/client/views/nodes/formattedText/DashDocCommentView.tsx b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
new file mode 100644
index 000000000..d94fe7fc6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocCommentView.tsx
@@ -0,0 +1,95 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IDashDocCommentView {
+ node: any;
+ view: any;
+ getPos: any;
+}
+
+export class DashDocCommentView extends React.Component<IDashDocCommentView>{
+ constructor(props: IDashDocCommentView) {
+ super(props);
+ }
+
+ targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = this.props.getPos() + 1; i < this.props.view.state.doc.content.size; i++) {
+ const m = this.props.view.state.doc.nodeAt(i);
+ if (m && m.type === this.props.view.state.schema.nodes.dashDoc && m.attrs.docid === this.props.node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = this.props.view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: this.props.node.attrs.docid, float: "right" });
+ this.props.view.dispatch(this.props.view.state.tr.insert(this.props.getPos() + 1, dashDoc));
+ setTimeout(() => { try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ }
+
+ onPointerDownCollapse = (e: any) => e.stopPropagation();
+
+ onPointerUpCollapse = (e: any) => {
+ const target = this.targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = this.props.view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ this.props.view.dispatch(tr.setSelection(TextSelection.create(tr.doc, this.props.getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { this.props.view.dispatch(this.props.view.state.tr.setSelection(TextSelection.create(this.props.view.state.tr.doc, this.props.getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ }
+
+ onPointerEnterCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ onPointerLeaveCollapse = (e: any) => {
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ }
+
+ render() {
+
+ const collapsedId = "DashDocCommentView-" + this.props.node.attrs.docid;
+
+ return (
+ <span
+ className="formattedTextBox-inlineComment"
+ id={collapsedId}
+ onPointerDown={this.onPointerDownCollapse}
+ onPointerUp={this.onPointerUpCollapse}
+ onPointerEnter={this.onPointerEnterCollapse}
+ onPointerLeave={this.onPointerLeaveCollapse}
+ >
+
+ </span >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx
new file mode 100644
index 000000000..9fe8fa320
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashDocView.tsx
@@ -0,0 +1,269 @@
+import { IReactionDisposer, reaction } from "mobx";
+import { NodeSelection } from "prosemirror-state";
+import { Doc, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+interface IDashDocView {
+ node: any;
+ view: any;
+ getPos: any;
+ tbox?: FormattedTextBox;
+ self: any;
+}
+
+export class DashDocView extends React.Component<IDashDocView> {
+
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+ _finalLayout: any;
+ _resolvedDataDoc: any;
+
+
+ // constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+
+ constructor(props: IDashDocView) {
+ super(props);
+
+ const node = this.props.node;
+ this._textBox = this.props.tbox as FormattedTextBox;
+
+ const alias = node.attrs.alias;
+ const docid = node.attrs.docid || this._textBox.props.Document[Id];
+
+ DocServer.GetRefField(docid + alias).then(async dashDoc => {
+ if (!(dashDoc instanceof Doc)) {
+ alias && DocServer.GetRefField(docid).then(async dashDocBase => {
+ if (dashDocBase instanceof Doc) {
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && DocumentView.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ this._dashDoc = aliasedDoc;
+ // self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ this._dashDoc = dashDoc;
+ // self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+
+ this.onPointerLeave = this.onPointerLeave.bind(this);
+ this.onPointerEnter = this.onPointerEnter.bind(this);
+ this.onKeyDown = this.onKeyDown.bind(this);
+ this.onKeyPress = this.onKeyPress.bind(this);
+ this.onKeyUp = this.onKeyUp.bind(this);
+ this.onWheel = this.onWheel.bind(this);
+ }
+ /* #region Internal functions */
+
+ removeDoc = () => {
+ const view = this.props.view;
+ const pos = this.props.getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ }
+
+ getDocTransform = () => {
+ const outerElement = document.getElementById('dash-document-view-outer') as HTMLElement;
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(outerElement);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+
+ outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
+
+ onKeyPress = (e: any) => {
+ e.stopPropagation();
+ }
+ onWheel = (e: any) => {
+ e.preventDefault();
+ }
+ onKeyUp = (e: any) => {
+ e.stopPropagation();
+ }
+ onKeyDown = (e: any) => {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ }
+ onPointerLeave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ }
+ onPointerEnter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + this.props.node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ }
+ /*endregion*/
+
+ componentWillMount = () => {
+ this._reactionDisposer?.();
+ }
+
+ componentDidUpdate = () => {
+
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+
+ const dashDoc = this._dashDoc as Doc;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = this.props.node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, this.props.node.attrs.fieldKey);
+
+ if (finalLayout) {
+ if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ finalLayout.rootDocument = dashDoc.aliasOf;
+ }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ this._finalLayout = finalLayout;
+ this._resolvedDataDoc = Cast(finalLayout.resolvedDataDoc, Doc, null);
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ }
+ },
+ (res) => {
+
+ if (res) {
+ this._finalLayout = res.finalLayout;
+ this._resolvedDataDoc = res.resolvedDataDoc;
+
+ this.forceUpdate(); // doReactRender(res.finalLayout, res.resolvedDataDoc),
+ }
+ },
+ { fireImmediately: true });
+
+ }
+
+ render() {
+ // doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+
+ const node = this.props.node;
+ const view = this.props.view;
+ const getPos = this.props.getPos;
+
+ const spanStyle = {
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ position: 'absolute' as 'absolute',
+ display: 'inline-block'
+ };
+
+
+ const outerStyle = {
+ position: "relative" as "relative",
+ textIndent: "0",
+ border: "1px solid " + StrCast(this._textBox.Document.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray")),
+ width: this.props.node.props.width,
+ height: this.props.node.props.height,
+ display: this.props.node.props.hidden ? "none" : "inline-block",
+ float: this.props.node.props.float,
+ };
+
+ const dashDoc = this._dashDoc as Doc;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+ const resolvedDataDoc = this._resolvedDataDoc; //Added this
+
+ if (!finalLayout) {
+ return <div></div>;
+ // if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ } else {
+
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() =>
+ ({
+ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()],
+ color: finalLayout.color
+ }),
+ ({ dim, color }) => {
+ spanStyle.width = outerStyle.width = Math.max(20, dim[0]) + "px";
+ spanStyle.height = outerStyle.height = Math.max(20, dim[1]) + "px";
+ outerStyle.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: true });
+
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+
+
+ //const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ // ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ return (
+ <span id="dash-document-view-outer"
+ className="outer"
+ style={outerStyle}
+ >
+ <div id="dashSpan"
+ className="dash-span"
+ style={spanStyle}
+ onPointerLeave={this.onPointerLeave}
+ onPointerEnter={this.onPointerEnter}
+ onKeyDown={this.onKeyDown}
+ onKeyPress={this.onKeyPress}
+ onKeyUp={this.onKeyUp}
+ onWheel={this.onWheel}
+ >
+ <DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={this.removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />
+
+ </div>
+ </span>
+ );
+
+ }
+ }
+
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.scss b/src/client/views/nodes/formattedText/DashFieldView.scss
new file mode 100644
index 000000000..35ff9c1e6
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.scss
@@ -0,0 +1,36 @@
+.dashFieldView {
+ position: relative;
+ display: inline-block;
+
+ .dashFieldView-enumerables {
+ width: 10px;
+ height: 10px;
+ position: relative;
+ display: inline-block;
+ background: dimGray;
+ }
+ .dashFieldView-fieldCheck {
+ min-width: 12px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ }
+ .dashFieldView-labelSpan {
+ position: relative;
+ display: inline-block;
+ font-size: small;
+ }
+ .dashFieldView-fieldSpan {
+ min-width: 20px;
+ margin-left: 2px;
+ margin-right: 5px;
+ position: relative;
+ display: inline-block;
+ background-color: rgba(155, 155, 155, 0.24);
+ span {
+ min-width: 100%;
+ display: inline-block;
+ }
+ }
+}
+ \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx
new file mode 100644
index 000000000..82c3185e7
--- /dev/null
+++ b/src/client/views/nodes/formattedText/DashFieldView.tsx
@@ -0,0 +1,211 @@
+import { IReactionDisposer, observable, runInAction, computed, action } from "mobx";
+import { Doc, DocListCast, Field } from "../../../../new_fields/Doc";
+import { List } from "../../../../new_fields/List";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, StrCast } from "../../../../new_fields/Types";
+import { DocServer } from "../../../DocServer";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import React = require("react");
+import * as ReactDOM from 'react-dom';
+import "./DashFieldView.scss";
+import { observer } from "mobx-react";
+
+
+export class DashFieldView {
+ _fieldWrapper: HTMLDivElement; // container for label and value
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._fieldWrapper = document.createElement("div");
+ this._fieldWrapper.style.width = node.attrs.width;
+ this._fieldWrapper.style.height = node.attrs.height;
+ this._fieldWrapper.style.fontWeight = "bold";
+ this._fieldWrapper.style.position = "relative";
+ this._fieldWrapper.style.display = "inline-block";
+ this._fieldWrapper.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeydown = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._fieldWrapper.onmousedown = function (e: any) { e.stopPropagation(); };
+
+ ReactDOM.render(<DashFieldViewInternal
+ fieldKey={node.attrs.fieldKey}
+ docid={node.attrs.docid}
+ width={node.attrs.width}
+ height={node.attrs.height}
+ view={view}
+ getPos={getPos}
+ tbox={tbox}
+ />, this._fieldWrapper);
+ (this as any).dom = this._fieldWrapper;
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._fieldWrapper);
+ }
+ selectNode() { }
+
+}
+interface IDashFieldViewInternal {
+ fieldKey: string;
+ docid: string;
+ view: any;
+ getPos: any;
+ tbox: FormattedTextBox;
+ width: number;
+ height: number;
+}
+
+@observer
+export class DashFieldViewInternal extends React.Component<IDashFieldViewInternal> {
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ _fieldKey: string;
+ _fieldStringRef = React.createRef<HTMLSpanElement>();
+ @observable _showEnumerables: boolean = false;
+ @observable _dashDoc: Doc | undefined;
+
+ constructor(props: IDashFieldViewInternal) {
+ super(props);
+ this._fieldKey = this.props.fieldKey;
+ this._textBoxDoc = this.props.tbox.props.Document;
+
+ if (this.props.docid) {
+ DocServer.GetRefField(this.props.docid).
+ then(action(async dashDoc => dashDoc instanceof Doc && (this._dashDoc = dashDoc)));
+ } else {
+ this._dashDoc = this.props.tbox.props.DataDoc || this.props.tbox.dataDoc;
+ }
+ }
+ componentWillUnmount() {
+ this._reactionDisposer?.();
+ }
+
+ // set the display of the field's value (checkbox for booleans, span of text for strings)
+ @computed get fieldValueContent() {
+ if (this._dashDoc) {
+ const dashVal = this._dashDoc[this._fieldKey];
+ const fval = StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(this._textBoxDoc)[this._fieldKey] : dashVal;
+ const boolVal = Cast(fval, "boolean", null);
+ const strVal = Field.toString(fval as Field) || "";
+
+ // field value is a boolean, so use a checkbox or similar widget to display it
+ if (boolVal === true || boolVal === false) {
+ return <input
+ className="dashFieldView-fieldCheck"
+ type="checkbox" checked={boolVal}
+ onChange={e => this._dashDoc![this._fieldKey] = e.target.checked}
+ />;
+ }
+ else // field value is a string, so display it as an editable span
+ {
+ // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't
+ // use React events. Essentially, React events occur after native events have been processed, so corresponding React events
+ // will never fire because Prosemirror has handled the native events. So we add listeners for native events here.
+ return <span contentEditable={true} ref={r => {
+ r?.addEventListener("keydown", e => this.fieldSpanKeyDown(e, r));
+ r?.addEventListener("blur", e => r && this.updateText(r.textContent!, false));
+ r?.addEventListener("pointerdown", action((e) => this._showEnumerables = true));
+ }}>
+ {strVal}
+ </span>;
+ }
+ }
+ }
+
+ // we need to handle all key events on the input span or else they will propagate to prosemirror.
+ @action
+ fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => {
+ if (e.key === "Enter") { // handle the enter key by "submitting" the current text to Dash's database.
+ e.ctrlKey && Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: span.textContent! }]);
+ this.updateText(span.textContent!, true);
+ e.preventDefault();// prevent default to avoid a newline from being generated and wiping out this field view
+ }
+ if (e.key === "a" && (e.ctrlKey || e.metaKey)) { // handle ctrl-A to select all the text within the span
+ if (window.getSelection) {
+ const range = document.createRange();
+ range.selectNodeContents(span);
+ window.getSelection()!.removeAllRanges();
+ window.getSelection()!.addRange(range);
+ }
+ e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected
+ }
+ e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror.
+ }
+
+ @action
+ updateText = (nodeText: string, forceMatch: boolean) => {
+ this._showEnumerables = false;
+ if (nodeText) {
+ const newText = nodeText.startsWith(":=") || nodeText.startsWith("=:=") ? ":=-computed-" : nodeText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(this._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText;
+ Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, []);
+ this._dashDoc![this._fieldKey] = modText;
+ } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ else if (nodeText.startsWith(":=")) {
+ this._dashDoc![this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2));
+ } else if (nodeText.startsWith("=:=")) {
+ Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3));
+ } else {
+ this._dashDoc![this._fieldKey] = newText;
+ }
+ });
+ }
+ }
+
+ // display a collection of all the enumerable values for this field
+ onPointerDownEnumerables = async (e: any) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(this._textBoxDoc, this._fieldKey, [{ title: this._fieldKey }]);
+ collview instanceof Doc && this.props.tbox.props.addDocTab(collview, "onRight");
+ }
+
+
+ // clicking on the label creates a pivot view collection of all documents
+ // in the same collection. The pivot field is the fieldKey of this label
+ onPointerDownLabelSpan = (e: any) => {
+ e.stopPropagation();
+ let container = this.props.tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = this._fieldKey;
+ this.props.tbox.props.addDocTab(alias, "onRight");
+ }
+ }
+
+ render() {
+ return <div className="dashFieldView" style={{
+ width: this.props.width,
+ height: this.props.height,
+ }}>
+ <span className="dashFieldView-labelSpan" title="click to see related tags" onPointerDown={this.onPointerDownLabelSpan}>
+ {this._fieldKey}
+ </span>
+
+ <div className="dashFieldView-fieldSpan">
+ {this.fieldValueContent}
+ </div>
+
+ {!this._showEnumerables ? (null) : <div className="dashFieldView-enumerables" onPointerDown={this.onPointerDownEnumerables} />}
+
+ </div >;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FootnoteView.tsx b/src/client/views/nodes/formattedText/FootnoteView.tsx
new file mode 100644
index 000000000..ee21fb765
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FootnoteView.tsx
@@ -0,0 +1,162 @@
+import { EditorView } from "prosemirror-view";
+import { EditorState } from "prosemirror-state";
+import { keymap } from "prosemirror-keymap";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { schema } from "./schema_rts";
+import { redo, undo } from "prosemirror-history";
+import { StepMap } from "prosemirror-transform";
+
+import React = require("react");
+
+interface IFootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+}
+
+export class FootnoteView extends React.Component<IFootnoteView> {
+ _innerView: any;
+ _node: any;
+
+ constructor(props: IFootnoteView) {
+ super(props);
+ const node = this.props.node;
+ const outerView = this.props.outerView;
+ const _innerView = this.props.innerView;
+ const getPos = this.props.getPos;
+ }
+
+ selectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.props.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.props.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.props.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.props.innerView.defineProperty(new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.props.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-y": () => redo(this.props.outerView.state, this.props.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.props.outerView.hasFocus()) this.props.innerView.focus();
+ }) as any
+ }
+ }));
+ setTimeout(() => this.props.innerView && this.props.innerView.docView.setSelection(0, 0, this.props.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.props.innerView.state.applyTransaction(tr);
+ this.props.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.props.outerView.state.tr, offsetMap = StepMap.offset(this.props.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.props.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.props.node)) return false;
+ this._node = node; //not sure
+ if (this.props.innerView) {
+ const state = this.props.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.props.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+ onPointerUp = (e: any) => {
+ this.toggle(e);
+ }
+
+ toggle = (e: any) => {
+ e.preventDefault();
+ if (this.props.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+
+ close() {
+ this.props.innerView && this.props.innerView.destroy();
+ this._innerView = null;
+ this.dom.textContent = "";
+ }
+
+ destroy() {
+ if (this.props.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.props.innerView && this.props.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+
+
+ render() {
+ return (
+ <div
+ className="footnote"
+ onPointerUp={this.onPointerUp}>
+ <div className="footnote-tooltip" >
+
+ </div >
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
index 3bedb7127..477a2ca08 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -1,4 +1,4 @@
-@import "../globalCssVariables";
+@import "../../globalCssVariables";
.ProseMirror {
width: 100%;
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 7f12aea29..248b4f467 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -12,39 +12,49 @@ import { Fragment, Mark, Node, 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 { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../new_fields/Doc";
-import { documentSchema } from '../../../new_fields/documentSchemas';
-import { Id } from '../../../new_fields/FieldSymbols';
-import { InkTool } from '../../../new_fields/InkField';
-import { PrefetchProxy } from '../../../new_fields/Proxy';
-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 { TraceMobx } from '../../../new_fields/util';
-import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } 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 { DragManager } from "../../util/DragManager";
-import { makeTemplate } from '../../util/DropConverter';
-import buildKeymap from "../../util/ProsemirrorExampleTransfer";
-import RichTextMenu from '../../util/RichTextMenu';
-import { RichTextRules } from "../../util/RichTextRules";
-import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, ImageResizeView, OrderedListView, schema, SummaryView } from "../../util/RichTextSchema";
-import { SelectionManager } from "../../util/SelectionManager";
-import { undoBatch, UndoManager } from "../../util/UndoManager";
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { ViewBoxAnnotatableComponent } from "../DocComponent";
-import { DocumentButtonBar } from '../DocumentButtonBar';
-import { InkingControl } from "../InkingControl";
-import { AudioBox } from './AudioBox';
-import { FieldView, FieldViewProps } from "./FieldView";
+import { DateField } from '../../../../new_fields/DateField';
+import { DataSym, Doc, DocListCast, DocListCastAsync, Field, HeightSym, Opt, WidthSym } from "../../../../new_fields/Doc";
+import { documentSchema } from '../../../../new_fields/documentSchemas';
+import { Id } from '../../../../new_fields/FieldSymbols';
+import { InkTool } from '../../../../new_fields/InkField';
+import { PrefetchProxy } from '../../../../new_fields/Proxy';
+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 { TraceMobx } from '../../../../new_fields/util';
+import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, numberRange, returnOne, returnZero, Utils } 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 { DragManager } from "../../../util/DragManager";
+import { makeTemplate } from '../../../util/DropConverter';
+import buildKeymap from "./ProsemirrorExampleTransfer";
+import RichTextMenu from './RichTextMenu';
+import { RichTextRules } from "./RichTextRules";
+import { DashDocCommentView, DashDocView, FootnoteView, ImageResizeView, OrderedListView, SummaryView } from "./RichTextSchema";
+// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "./RichTextSchema";
+// import { OrderedListView } from "./RichTextSchema";
+// import { ImageResizeView } from "./ImageResizeView";
+// import { DashDocCommentView } from "./DashDocCommentView";
+// import { FootnoteView } from "./FootnoteView";
+// import { SummaryView } from "./SummaryView";
+// import { DashDocView } from "./DashDocView";
+import { DashFieldView } from "./DashFieldView";
+
+import { schema } from "./schema_rts";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
+import { CollectionFreeFormView } from '../../collections/collectionFreeForm/CollectionFreeFormView';
+import { ContextMenu } from '../../ContextMenu';
+import { ContextMenuProps } from '../../ContextMenuItem';
+import { ViewBoxAnnotatableComponent } from "../../DocComponent";
+import { DocumentButtonBar } from '../../DocumentButtonBar';
+import { InkingControl } from "../../InkingControl";
+import { AudioBox } from '../AudioBox';
+import { FieldView, FieldViewProps } from "../FieldView";
import "./FormattedTextBox.scss";
import { FormattedTextBoxComment, formattedTextBoxCommentPlugin } from './FormattedTextBoxComment';
import React = require("react");
@@ -869,7 +879,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
dashComment(node, view, getPos) { return new DashDocCommentView(node, view, getPos); },
dashField(node, view, getPos) { return new DashFieldView(node, view, getPos, self); },
dashDoc(node, view, getPos) { return new DashDocView(node, view, getPos, self); },
- image(node, view, getPos) { return new ImageResizeView(node, view, getPos, self.props.addDocTab); },
+ // dashDoc(node, view, getPos) { return new DashDocView({ node, view, getPos, self }); },
+
+ // image(node, view, getPos) {
+ // //const addDocTab = this.props.addDocTab;
+ // return new ImageResizeView({ node, view, getPos, addDocTab: this.props.addDocTab });
+ // },
+ // // WAS :
+ // //image(node, view, getPos) { return new ImageResizeView(node, view, getPos, this.props.addDocTab); },
+
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); }
@@ -1203,6 +1221,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
FormattedTextBoxComment.Hide();
}
return (
+
<div className={`formattedTextBox-cont`} ref={this._ref}
style={{
height: this.layoutDoc._autoHeight && this.props.renderDepth ? "max-content" : `calc(100% - ${this.props.ChromeHeight?.() || 0}px`,
@@ -1224,7 +1243,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onMouseUp={this.onMouseUp}
onWheel={this.onPointerWheel}
onPointerEnter={action(() => this._entered = true)}
- onPointerLeave={action(() => this._entered = false)}
+ onPointerLeave={action((e: React.PointerEvent<HTMLDivElement>) => {
+ this._entered = false;
+ const target = document.elementFromPoint(e.nativeEvent.x, e.nativeEvent.y);
+ for (let child: any = target; child; child = child?.parentElement) {
+ if (child === this._ref.current!) {
+ this._entered = true;
+ }
+ }
+ })}
>
<div className={`formattedTextBox-outer`} style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, }} onScroll={this.onscrolled} ref={this._scrollRef}>
<div className={`formattedTextBox-inner${rounded}`} ref={this.createDropTarget}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
index 2dd63ec21..2dd63ec21 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.scss
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
index 7d441a48b..f9e4c5210 100644
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -2,20 +2,20 @@ import { Mark, ResolvedPos } from "prosemirror-model";
import { EditorState, Plugin } from "prosemirror-state";
import { EditorView } from "prosemirror-view";
import * as ReactDOM from 'react-dom';
-import { Doc, DocCastAsync } from "../../../new_fields/Doc";
-import { Cast, FieldValue, NumCast } from "../../../new_fields/Types";
-import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../Utils";
-import { DocServer } from "../../DocServer";
-import { DocumentManager } from "../../util/DocumentManager";
-import { schema } from "../../util/RichTextSchema";
-import { Transform } from "../../util/Transform";
-import { ContentFittingDocumentView } from "./ContentFittingDocumentView";
+import { Doc, DocCastAsync } from "../../../../new_fields/Doc";
+import { Cast, FieldValue, NumCast } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, Utils, emptyPath } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { schema } from "./schema_rts";
+import { Transform } from "../../../util/Transform";
+import { ContentFittingDocumentView } from "../ContentFittingDocumentView";
import { FormattedTextBox } from "./FormattedTextBox";
import './FormattedTextBoxComment.scss';
import React = require("react");
-import { Docs } from "../../documents/Documents";
+import { Docs } from "../../../documents/Documents";
import wiki from "wikijs";
-import { DocumentType } from "../../documents/DocumentTypes";
+import { DocumentType } from "../../../documents/DocumentTypes";
export let formattedTextBoxCommentPlugin = new Plugin({
view(editorView) { return new FormattedTextBoxComment(editorView); }
diff --git a/src/client/views/nodes/formattedText/ImageResizeView.tsx b/src/client/views/nodes/formattedText/ImageResizeView.tsx
new file mode 100644
index 000000000..8f98da0fd
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ImageResizeView.tsx
@@ -0,0 +1,138 @@
+import { NodeSelection } from "prosemirror-state";
+import { Doc } from "../../../../new_fields/Doc";
+import { DocServer } from "../../../DocServer";
+import { DocumentManager } from "../../../util/DocumentManager";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+interface IImageResizeView {
+ node: any;
+ view: any;
+ getPos: any;
+ addDocTab: any;
+}
+
+export class ImageResizeView extends React.Component<IImageResizeView> {
+ constructor(props: IImageResizeView) {
+ super(props);
+ }
+
+ onClickImg = (e: any) => {
+ e.stopPropagation();
+ e.preventDefault();
+ if (this.props.view.state.selection.node && this.props.view.state.selection.node.type !== this.props.view.state.schema.nodes.image) {
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(this.props.view.state.selection.from - 2))));
+ }
+ }
+
+ onPointerDownImg = (e: any) => {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(this.props.node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, this.props.view.state.schema.Document,
+ document => this.props.addDocTab(document, this.props.node.attrs.location ? this.props.node.attrs.location : "inTab"), false));
+ }
+ }
+
+ onPointerDownHandle = (e: any) => {
+ e.preventDefault();
+ e.stopPropagation();
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const wid = Number(getComputedStyle(elementImage).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(elementImage).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(this.props.node.attrs.width);
+
+ const onpointermove = (e: any) => {
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ elementOuter.style.width = `${startWidth + diffInPx}`;
+ elementOuter.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = this.props.view.state.selection.from;
+ const elementOuter = document.getElementById("outerId") as HTMLElement;
+ this.props.view.dispatch(this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, width: elementOuter.style.width, height: elementOuter.style.height }));
+ this.props.view.dispatch(this.props.view.state.tr.setSelection(new NodeSelection(this.props.view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ }
+
+ selectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.add("ProseMirror-selectednode");
+ elementHandle.style.display = "";
+ }
+
+ deselectNode() {
+ const elementImage = document.getElementById("imageId") as HTMLElement;
+ const elementHandle = document.getElementById("handleId") as HTMLElement;
+
+ elementImage.classList.remove("ProseMirror-selectednode");
+ elementHandle.style.display = "none";
+ }
+
+
+ render() {
+
+ const outerStyle = {
+ width: this.props.node.attrs.width,
+ height: this.props.node.attrs.height,
+ display: "inline-block",
+ overflow: "hidden",
+ float: this.props.node.attrs.float
+ };
+
+ const imageStyle = {
+ width: "100%",
+ };
+
+ const handleStyle = {
+ position: "absolute",
+ width: "20px",
+ heiht: "20px",
+ backgroundColor: "blue",
+ borderRadius: "15px",
+ display: "none",
+ bottom: "-10px",
+ right: "-10px"
+
+ };
+
+
+
+ return (
+ <div id="outer"
+ style={outerStyle}
+ >
+ <img
+ id="imageId"
+ style={imageStyle}
+ src={this.props.node.src}
+ onClick={this.onClickImg}
+ onPointerDown={this.onPointerDownImg}
+
+ >
+ </img>
+ <span
+ id="handleId"
+ onPointerDown={this.onPointerDownHandle}
+ >
+
+ </span>
+ </div >
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
new file mode 100644
index 000000000..d80e64634
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ParagraphNodeSpec.ts
@@ -0,0 +1,143 @@
+import clamp from '../../../util/clamp';
+import convertToCSSPTValue from '../../../util/convertToCSSPTValue';
+import toCSSLineSpacing from '../../../util/toCSSLineSpacing';
+import { Node, DOMOutputSpec } from 'prosemirror-model';
+
+//import type { NodeSpec } from './Types';
+type NodeSpec = {
+ attrs?: { [key: string]: any },
+ content?: string,
+ draggable?: boolean,
+ group?: string,
+ inline?: boolean,
+ name?: string,
+ parseDOM?: Array<any>,
+ toDOM?: (node: any) => DOMOutputSpec,
+};
+
+// This assumes that every 36pt maps to one indent level.
+export const INDENT_MARGIN_PT_SIZE = 36;
+export const MIN_INDENT_LEVEL = 0;
+export const MAX_INDENT_LEVEL = 7;
+export const ATTRIBUTE_INDENT = 'data-indent';
+
+export const EMPTY_CSS_VALUE = new Set(['', '0%', '0pt', '0px']);
+
+const ALIGN_PATTERN = /(left|right|center|justify)/;
+
+// https://github.com/ProseMirror/prosemirror-schema-basic/blob/master/src/schema-basic.js
+// :: NodeSpec A plain paragraph textblock. Represented in the DOM
+// as a `<p>` element.
+const ParagraphNodeSpec: NodeSpec = {
+ attrs: {
+ align: { default: null },
+ color: { default: null },
+ id: { default: null },
+ indent: { default: null },
+ inset: { default: null },
+ lineSpacing: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingBottom: { default: null },
+ // TODO: Add UI to let user edit / clear padding.
+ paddingTop: { default: null },
+ },
+ content: 'inline*',
+ group: 'block',
+ parseDOM: [{ tag: 'p', getAttrs }],
+ toDOM,
+};
+
+function getAttrs(dom: HTMLElement): Object {
+ const {
+ lineHeight,
+ textAlign,
+ marginLeft,
+ paddingTop,
+ paddingBottom,
+ } = dom.style;
+
+ let align = dom.getAttribute('align') || textAlign || '';
+ align = ALIGN_PATTERN.test(align) ? align : "";
+
+ let indent = parseInt(dom.getAttribute(ATTRIBUTE_INDENT) || "", 10);
+
+ if (!indent && marginLeft) {
+ indent = convertMarginLeftToIndentValue(marginLeft);
+ }
+
+ indent = indent || MIN_INDENT_LEVEL;
+
+ const lineSpacing = lineHeight ? toCSSLineSpacing(lineHeight) : null;
+
+ const id = dom.getAttribute('id') || '';
+ return { align, indent, lineSpacing, paddingTop, paddingBottom, id };
+}
+
+function toDOM(node: Node): DOMOutputSpec {
+ const {
+ align,
+ indent,
+ inset,
+ lineSpacing,
+ paddingTop,
+ paddingBottom,
+ id,
+ } = node.attrs;
+ const attrs: { [key: string]: any } | null = {};
+
+ let style = '';
+ if (align && align !== 'left') {
+ style += `text-align: ${align};`;
+ }
+
+ if (lineSpacing) {
+ const cssLineSpacing = toCSSLineSpacing(lineSpacing);
+ style +=
+ `line-height: ${cssLineSpacing};` +
+ // This creates the local css variable `--czi-content-line-height`
+ // that its children may apply.
+ `--czi-content-line-height: ${cssLineSpacing}`;
+ }
+
+ if (paddingTop && !EMPTY_CSS_VALUE.has(paddingTop)) {
+ style += `padding-top: ${paddingTop};`;
+ }
+
+ if (paddingBottom && !EMPTY_CSS_VALUE.has(paddingBottom)) {
+ style += `padding-bottom: ${paddingBottom};`;
+ }
+
+ if (indent) {
+ style += `text-indent: ${indent}; padding-left: ${indent < 0 ? -indent : undefined};`;
+ }
+
+ if (inset) {
+ style += `margin-left: ${inset}; margin-right: ${inset};`;
+ }
+
+ style && (attrs.style = style);
+
+ if (indent) {
+ attrs[ATTRIBUTE_INDENT] = String(indent);
+ }
+
+ if (id) {
+ attrs.id = id;
+ }
+
+ return ['p', attrs, 0];
+}
+
+export const toParagraphDOM = toDOM;
+export const getParagraphNodeAttrs = getAttrs;
+
+export function convertMarginLeftToIndentValue(marginLeft: string): number {
+ const ptValue = convertToCSSPTValue(marginLeft);
+ return clamp(
+ MIN_INDENT_LEVEL,
+ Math.floor(ptValue / INDENT_MARGIN_PT_SIZE),
+ MAX_INDENT_LEVEL
+ );
+}
+
+export default ParagraphNodeSpec; \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
new file mode 100644
index 000000000..a0b02880e
--- /dev/null
+++ b/src/client/views/nodes/formattedText/ProsemirrorExampleTransfer.ts
@@ -0,0 +1,241 @@
+import { chainCommands, exitCode, joinDown, joinUp, lift, selectParentNode, setBlockType, splitBlockKeepMarks, toggleMark, wrapIn } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { undoInputRule } from "prosemirror-inputrules";
+import { Schema } from "prosemirror-model";
+import { liftListItem, sinkListItem } from "./prosemirrorPatches.js";
+import { splitListItem, wrapInList, } from "prosemirror-schema-list";
+import { EditorState, Transaction, TextSelection } from "prosemirror-state";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { Docs } from "../../../documents/Documents";
+import { NumCast, BoolCast, Cast, StrCast } from "../../../../new_fields/Types";
+import { Doc } from "../../../../new_fields/Doc";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { Id } from "../../../../new_fields/FieldSymbols";
+
+const mac = typeof navigator !== "undefined" ? /Mac/.test(navigator.platform) : false;
+
+export type KeyMap = { [key: string]: any };
+
+export let updateBullets = (tx2: Transaction, schema: Schema, mapStyle?: string) => {
+ let fontSize: number | undefined = undefined;
+ tx2.doc.descendants((node: any, offset: any, index: any) => {
+ if (node.type === schema.nodes.ordered_list || node.type === schema.nodes.list_item) {
+ const path = (tx2.doc.resolve(offset) as any).path;
+ let depth = Array.from(path).reduce((p: number, c: any) => p + (c.hasOwnProperty("type") && c.type === schema.nodes.ordered_list ? 1 : 0), 0);
+ if (node.type === schema.nodes.ordered_list) depth++;
+ fontSize = depth === 1 && node.attrs.setFontSize ? Number(node.attrs.setFontSize) : fontSize;
+ const fsize = fontSize && node.type === schema.nodes.ordered_list ? Math.max(6, fontSize - (depth - 1) * 4) : undefined;
+ tx2.setNodeMarkup(offset, node.type, { ...node.attrs, mapStyle: mapStyle ? mapStyle : node.attrs.mapStyle, bulletStyle: depth, inheritedFontSize: fsize }, node.marks);
+ }
+ });
+ return tx2;
+};
+export default function buildKeymap<S extends Schema<any>>(schema: S, props: any, mapKeys?: KeyMap): KeyMap {
+ const keys: { [key: string]: any } = {};
+
+ function bind(key: string, cmd: any) {
+ if (mapKeys) {
+ const mapped = mapKeys[key];
+ if (mapped === false) return;
+ if (mapped) key = mapped;
+ }
+ keys[key] = cmd;
+ }
+
+ bind("Mod-z", undo);
+ bind("Shift-Mod-z", redo);
+ bind("Backspace", undoInputRule);
+
+ !mac && bind("Mod-y", redo);
+
+ bind("Alt-ArrowUp", joinUp);
+ bind("Alt-ArrowDown", joinDown);
+ bind("Mod-BracketLeft", lift);
+ bind("Escape", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ (document.activeElement as any).blur?.();
+ SelectionManager.DeselectAll();
+ });
+
+ bind("Mod-b", toggleMark(schema.marks.strong));
+ bind("Mod-B", toggleMark(schema.marks.strong));
+
+ bind("Mod-e", toggleMark(schema.marks.em));
+ bind("Mod-E", toggleMark(schema.marks.em));
+
+ bind("Mod-u", toggleMark(schema.marks.underline));
+ bind("Mod-U", toggleMark(schema.marks.underline));
+
+ bind("Mod-`", toggleMark(schema.marks.code));
+
+ bind("Ctrl-.", wrapInList(schema.nodes.bullet_list));
+
+ bind("Ctrl-n", wrapInList(schema.nodes.ordered_list));
+
+ bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+
+ // bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ // let newNode = schema.nodes.footnote.create({});
+ // if (dispatch && state.selection.from === state.selection.to) {
+ // let tr = state.tr;
+ // tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ // dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ // tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ // tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+ // return true;
+ // }
+ // return false;
+ // });
+
+
+ const cmd = chainCommands(exitCode, (state, dispatch) => {
+ if (dispatch) {
+ dispatch(state.tr.replaceSelectionWith(schema.nodes.hard_break.create()).scrollIntoView());
+ return true;
+ }
+ return false;
+ });
+ bind("Mod-Enter", cmd);
+ bind("Shift-Enter", cmd);
+ mac && bind("Ctrl-Enter", cmd);
+
+
+ bind("Shift-Ctrl-0", setBlockType(schema.nodes.paragraph));
+
+ bind("Shift-Ctrl-\\", setBlockType(schema.nodes.code_block));
+
+ for (let i = 1; i <= 6; i++) {
+ bind("Shift-Ctrl-" + i, setBlockType(schema.nodes.heading, { level: i }));
+ }
+
+ const hr = schema.nodes.horizontal_rule;
+ bind("Mod-_", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ bind("Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const ref = state.selection;
+ const range = ref.$from.blockRange(ref.$to);
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!sinkListItem(schema.nodes.list_item)(state, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) { // couldn't sink into an existing list, so wrap in a new one
+ const newstate = state.applyTransaction(state.tr.setSelection(TextSelection.create(state.doc, range!.start, range!.end)));
+ if (!wrapInList(schema.nodes.ordered_list)(newstate.state, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ // when promoting to a list, assume list will format things so don't copy the stored marks.
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet promote fail");
+ }
+ }
+ });
+
+ bind("Shift-Tab", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+
+ if (!liftListItem(schema.nodes.list_item)(state.tr, (tx2: Transaction) => {
+ const tx3 = updateBullets(tx2, schema);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+ dispatch(tx3);
+ })) {
+ console.log("bullet demote fail");
+ }
+ });
+ bind("Ctrl-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (originalDoc instanceof Doc) {
+ const layoutKey = StrCast(originalDoc.layoutKey);
+ const newDoc = Docs.Create.TextDocument("", {
+ layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+ layoutKey,
+ _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x), y: NumCast(originalDoc.y) + NumCast(originalDoc._height) + 10, _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+ newDoc[layoutKey] = originalDoc[layoutKey];
+ }
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ }
+ });
+
+ const splitMetadata = (marks: any, tx: Transaction) => {
+ marks && tx.ensureMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ marks && tx.setStoredMarks(marks.filter((val: any) => val.type !== schema.marks.metadata && val.type !== schema.marks.metadataKey && val.type !== schema.marks.metadataVal));
+ return tx;
+ };
+ const addTextOnRight = (force: boolean) => {
+ const layoutDoc = props.Document;
+ const originalDoc = layoutDoc.rootDocument || layoutDoc;
+ if (force || props.Document._singleLine) {
+ const layoutKey = StrCast(originalDoc.layoutKey);
+ const newDoc = Docs.Create.TextDocument("", {
+ layout: Cast(originalDoc.layout, Doc, null) || FormattedTextBox.DefaultLayout,
+ layoutKey,
+ _singleLine: BoolCast(originalDoc._singleLine),
+ x: NumCast(originalDoc.x) + NumCast(originalDoc._width) + 10, y: NumCast(originalDoc.y), _width: NumCast(layoutDoc._width), _height: NumCast(layoutDoc._height)
+ });
+ if (layoutKey !== "layout" && originalDoc[layoutKey] instanceof Doc) {
+ newDoc[layoutKey] = originalDoc[layoutKey];
+ }
+ FormattedTextBox.SelectOnLoad = newDoc[Id];
+ props.addDocument(newDoc);
+ return true;
+ }
+ return false;
+ };
+ bind("Alt-Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ return addTextOnRight(true);
+ });
+ bind("Enter", (state: EditorState<S>, dispatch: (tx: Transaction<Schema<any, any>>) => void) => {
+ if (addTextOnRight(false)) return true;
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ if (!splitListItem(schema.nodes.list_item)(state, dispatch)) {
+ if (!splitBlockKeepMarks(state, (tx3: Transaction) => {
+ splitMetadata(marks, tx3);
+ if (!liftListItem(schema.nodes.list_item)(tx3, dispatch as ((tx: Transaction<Schema<any, any>>) => void))) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
+ bind("Space", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ dispatch(splitMetadata(marks, state.tr));
+ return false;
+ });
+ bind(":", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ const range = state.selection.$from.blockRange(state.selection.$to, (node: any) => {
+ return !node.marks || !node.marks.find((m: any) => m.type === schema.marks.metadata);
+ });
+ const path = (state.doc.resolve(state.selection.from - 1) as any).path;
+ const spaceSeparator = path[path.length - 3].childCount > 1 ? 0 : -1;
+ const anchor = range!.end - path[path.length - 3].lastChild.nodeSize + spaceSeparator;
+ if (anchor >= 0) {
+ const textsel = TextSelection.create(state.doc, anchor, range!.end);
+ const text = range ? state.doc.textBetween(textsel.from, textsel.to) : "";
+ let whitespace = text.length - 1;
+ for (; whitespace >= 0 && text[whitespace] !== " "; whitespace--) { }
+ if (text.endsWith(":")) {
+ dispatch(state.tr.addMark(textsel.from + whitespace + 1, textsel.to, schema.marks.metadata.create() as any).
+ addMark(textsel.from + whitespace + 1, textsel.to - 2, schema.marks.metadataKey.create() as any));
+ }
+ }
+ return false;
+ });
+
+
+ return keys;
+}
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.scss b/src/client/views/nodes/formattedText/RichTextMenu.scss
new file mode 100644
index 000000000..36da769c3
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextMenu.scss
@@ -0,0 +1,121 @@
+@import "../../globalCssVariables";
+
+.button-dropdown-wrapper {
+ position: relative;
+
+ .dropdown-button {
+ width: 15px;
+ padding-left: 5px;
+ padding-right: 5px;
+ }
+
+ .dropdown-button-combined {
+ width: 50px;
+ display: flex;
+ justify-content: space-between;
+
+ svg:nth-child(2) {
+ margin-top: 2px;
+ }
+ }
+
+ .dropdown {
+ position: absolute;
+ top: 35px;
+ left: 0;
+ background-color: #323232;
+ color: $light-color-secondary;
+ border: 1px solid #4d4d4d;
+ border-radius: 0 6px 6px 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+ min-width: 150px;
+ padding: 5px;
+ font-size: 12px;
+ z-index: 10001;
+
+ button {
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+ padding: 6px;
+ margin: 5px 0;
+ font-size: 10px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &:last-child {
+ margin-bottom: 0;
+ }
+ }
+ }
+
+ input {
+ color: black;
+ }
+}
+
+.link-menu {
+ .divider {
+ background-color: white;
+ height: 1px;
+ width: 100%;
+ }
+}
+
+.color-preview-button {
+ .color-preview {
+ width: 100%;
+ height: 3px;
+ margin-top: 3px;
+ }
+}
+
+.color-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+
+ button.color-button {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: 2px solid transparent !important;
+ padding: 3px;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+ }
+}
+
+select {
+ background-color: #323232;
+ color: white;
+ border: 1px solid black;
+ // border-top: none;
+ // border-bottom: none;
+ font-size: 12px;
+ height: 100%;
+ margin-right: 3px;
+
+ &:focus,
+ &:hover {
+ background-color: black;
+ }
+
+ &::-ms-expand {
+ color: white;
+ }
+}
+
+.row-2 {
+ display: flex;
+ justify-content: space-between;
+
+ >div {
+ display: flex;
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
new file mode 100644
index 000000000..cc04e0d6d
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -0,0 +1,875 @@
+import React = require("react");
+import AntimodeMenu from "../../AntimodeMenu";
+import { observable, action, } from "mobx";
+import { observer } from "mobx-react";
+import { Mark, MarkType, Node as ProsNode, NodeType, ResolvedPos, Schema } from "prosemirror-model";
+import { schema } from "./schema_rts";
+import { EditorView } from "prosemirror-view";
+import { EditorState, NodeSelection, TextSelection } from "prosemirror-state";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { IconProp, library } from '@fortawesome/fontawesome-svg-core';
+import { faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSubscript, faSuperscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller, faSleigh } from "@fortawesome/free-solid-svg-icons";
+import { updateBullets } from "./ProsemirrorExampleTransfer";
+import { FieldViewProps } from "../FieldView";
+import { Cast, StrCast } from "../../../../new_fields/Types";
+import { FormattedTextBoxProps } from "./FormattedTextBox";
+import { unimplementedFunction, Utils } from "../../../../Utils";
+import { wrapInList } from "prosemirror-schema-list";
+import { PastelSchemaPalette, DarkPastelSchemaPalette } from '../../../../new_fields/SchemaHeaderField';
+import "./RichTextMenu.scss";
+import { DocServer } from "../../../DocServer";
+import { Doc } from "../../../../new_fields/Doc";
+import { SelectionManager } from "../../../util/SelectionManager";
+import { LinkManager } from "../../../util/LinkManager";
+const { toggleMark, setBlockType } = require("prosemirror-commands");
+
+library.add(faBold, faItalic, faChevronLeft, faUnderline, faStrikethrough, faSuperscript, faSubscript, faIndent, faEyeDropper, faCaretDown, faPalette, faHighlighter, faLink, faPaintRoller);
+
+@observer
+export default class RichTextMenu extends AntimodeMenu {
+ static Instance: RichTextMenu;
+ public overMenu: boolean = false; // kind of hacky way to prevent selects not being selectable
+
+ private view?: EditorView;
+ public editorProps: FieldViewProps & FormattedTextBoxProps | undefined;
+
+ public _brushMap: Map<string, Set<Mark>> = new Map();
+ private fontSizeOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private fontFamilyOptions: { mark: Mark | null, title: string, label: string, command: any, hidden?: boolean, style?: {} }[];
+ private listTypeOptions: { node: NodeType | any | null, title: string, label: string, command: any, style?: {} }[];
+ private fontColors: (string | undefined)[];
+ private highlightColors: (string | undefined)[];
+
+ @observable private collapsed: boolean = false;
+ @observable private boldActive: boolean = false;
+ @observable private italicsActive: boolean = false;
+ @observable private underlineActive: boolean = false;
+ @observable private strikethroughActive: boolean = false;
+ @observable private subscriptActive: boolean = false;
+ @observable private superscriptActive: boolean = false;
+
+ @observable private activeFontSize: string = "";
+ @observable private activeFontFamily: string = "";
+ @observable private activeListType: string = "";
+
+ @observable private brushIsEmpty: boolean = true;
+ @observable private brushMarks: Set<Mark> = new Set();
+ @observable private showBrushDropdown: boolean = false;
+
+ @observable private activeFontColor: string = "black";
+ @observable private showColorDropdown: boolean = false;
+
+ @observable private activeHighlightColor: string = "transparent";
+ @observable private showHighlightDropdown: boolean = false;
+
+ @observable private currentLink: string | undefined = "";
+ @observable private showLinkDropdown: boolean = false;
+
+ constructor(props: Readonly<{}>) {
+ super(props);
+ RichTextMenu.Instance = this;
+ this._canFade = false;
+
+ this.fontSizeOptions = [
+ { mark: schema.marks.pFontSize.create({ fontSize: 7 }), title: "Set font size", label: "7pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 8 }), title: "Set font size", label: "8pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 9 }), title: "Set font size", label: "9pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 10 }), title: "Set font size", label: "10pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 12 }), title: "Set font size", label: "12pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 14 }), title: "Set font size", label: "14pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 16 }), title: "Set font size", label: "16pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 18 }), title: "Set font size", label: "18pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 20 }), title: "Set font size", label: "20pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 24 }), title: "Set font size", label: "24pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 32 }), title: "Set font size", label: "32pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 48 }), title: "Set font size", label: "48pt", command: this.changeFontSize },
+ { mark: schema.marks.pFontSize.create({ fontSize: 72 }), title: "Set font size", label: "72pt", command: this.changeFontSize },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ { mark: null, title: "", label: "13pt", command: unimplementedFunction, hidden: true }, // this is here because the default size is 13, but there is no actual 13pt option
+ ];
+
+ this.fontFamilyOptions = [
+ { mark: schema.marks.pFontFamily.create({ family: "Times New Roman" }), title: "Set font family", label: "Times New Roman", command: this.changeFontFamily, style: { fontFamily: "Times New Roman" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Arial" }), title: "Set font family", label: "Arial", command: this.changeFontFamily, style: { fontFamily: "Arial" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Georgia" }), title: "Set font family", label: "Georgia", command: this.changeFontFamily, style: { fontFamily: "Georgia" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Comic Sans MS" }), title: "Set font family", label: "Comic Sans MS", command: this.changeFontFamily, style: { fontFamily: "Comic Sans MS" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Tahoma" }), title: "Set font family", label: "Tahoma", command: this.changeFontFamily, style: { fontFamily: "Tahoma" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Impact" }), title: "Set font family", label: "Impact", command: this.changeFontFamily, style: { fontFamily: "Impact" } },
+ { mark: schema.marks.pFontFamily.create({ family: "Crimson Text" }), title: "Set font family", label: "Crimson Text", command: this.changeFontFamily, style: { fontFamily: "Crimson Text" } },
+ { mark: null, title: "", label: "various", command: unimplementedFunction, hidden: true },
+ // { mark: null, title: "", label: "default", command: unimplementedFunction, hidden: true },
+ ];
+
+ this.listTypeOptions = [
+ { node: schema.nodes.ordered_list.create({ mapStyle: "bullet" }), title: "Set list type", label: ":", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "decimal" }), title: "Set list type", label: "1.1", command: this.changeListType },
+ { node: schema.nodes.ordered_list.create({ mapStyle: "multi" }), title: "Set list type", label: "1.A", command: this.changeListType },
+ { node: undefined, title: "Set list type", label: "Remove", command: this.changeListType },
+ ];
+
+ this.fontColors = [
+ DarkPastelSchemaPalette.get("pink2"),
+ DarkPastelSchemaPalette.get("purple4"),
+ DarkPastelSchemaPalette.get("bluegreen1"),
+ DarkPastelSchemaPalette.get("yellow4"),
+ DarkPastelSchemaPalette.get("red2"),
+ DarkPastelSchemaPalette.get("bluegreen7"),
+ DarkPastelSchemaPalette.get("bluegreen5"),
+ DarkPastelSchemaPalette.get("orange1"),
+ "#757472",
+ "#000"
+ ];
+
+ this.highlightColors = [
+ PastelSchemaPalette.get("pink2"),
+ PastelSchemaPalette.get("purple4"),
+ PastelSchemaPalette.get("bluegreen1"),
+ PastelSchemaPalette.get("yellow4"),
+ PastelSchemaPalette.get("red2"),
+ PastelSchemaPalette.get("bluegreen7"),
+ PastelSchemaPalette.get("bluegreen5"),
+ PastelSchemaPalette.get("orange1"),
+ "white",
+ "transparent"
+ ];
+ }
+
+ @action
+ changeView(view: EditorView) {
+ this.view = view;
+ }
+
+ update(view: EditorView, lastState: EditorState | undefined) {
+ this.updateFromDash(view, lastState, this.editorProps);
+ }
+
+
+ public MakeLinkToSelection = (linkDocId: string, title: string, location: string, targetDocId: string): string => {
+ if (this.view) {
+ const link = this.view.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link).
+ addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ return this.view.state.selection.$from.nodeAfter?.text || "";
+ }
+ return "";
+ }
+
+ @action
+ public async updateFromDash(view: EditorView, lastState: EditorState | undefined, props: any) {
+ if (!view) {
+ console.log("no editor? why?");
+ return;
+ }
+ this.view = view;
+ const state = view.state;
+ props && (this.editorProps = props);
+
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) && lastState.selection.eq(state.selection)) return;
+
+ // update active marks
+ const activeMarks = this.getActiveMarksOnSelection();
+ this.setActiveMarkButtons(activeMarks);
+
+ // update active font family and size
+ const active = this.getActiveFontStylesOnSelection();
+ const activeFamilies = active && active.get("families");
+ const activeSizes = active && active.get("sizes");
+
+ this.activeFontFamily = !activeFamilies || activeFamilies.length === 0 ? "Arial" : activeFamilies.length === 1 ? String(activeFamilies[0]) : "various";
+ this.activeFontSize = !activeSizes || activeSizes.length === 0 ? "13pt" : activeSizes.length === 1 ? String(activeSizes[0]) + "pt" : "various";
+
+ // update link in current selection
+ const targetTitle = await this.getTextLinkTargetTitle();
+ this.setCurrentLink(targetTitle);
+ }
+
+ setMark = (mark: Mark, state: EditorState<any>, dispatch: any) => {
+ if (mark) {
+ const node = (state.selection as NodeSelection).node;
+ if (node?.type === schema.nodes.ordered_list) {
+ let attrs = node.attrs;
+ if (mark.type === schema.marks.pFontFamily) attrs = { ...attrs, setFontFamily: mark.attrs.family };
+ if (mark.type === schema.marks.pFontSize) attrs = { ...attrs, setFontSize: mark.attrs.fontSize };
+ if (mark.type === schema.marks.pFontColor) attrs = { ...attrs, setFontColor: mark.attrs.color };
+ const tr = updateBullets(state.tr.setNodeMarkup(state.selection.from, node.type, attrs), state.schema);
+ dispatch(tr.setSelection(new NodeSelection(tr.doc.resolve(state.selection.from))));
+ } else {
+ toggleMark(mark.type, mark.attrs)(state, (tx: any) => {
+ const { from, $from, to, empty } = tx.selection;
+ if (!tx.doc.rangeHasMark(from, to, mark.type)) {
+ toggleMark(mark.type, mark.attrs)({ tr: tx, doc: tx.doc, selection: tx.selection, storedMarks: tx.storedMarks }, dispatch);
+ } else dispatch(tx);
+ });
+ }
+ }
+ }
+
+ // finds font sizes and families in selection
+ getActiveFontStylesOnSelection() {
+ if (!this.view) return;
+
+ const activeFamilies: string[] = [];
+ const activeSizes: string[] = [];
+ const state = this.view.state;
+ const pos = this.view.state.selection.$from;
+ const ref_node = this.reference_node(pos);
+ if (ref_node && ref_node !== this.view.state.doc && ref_node.isText) {
+ ref_node.marks.forEach(m => {
+ m.type === state.schema.marks.pFontFamily && activeFamilies.push(m.attrs.family);
+ m.type === state.schema.marks.pFontSize && activeSizes.push(String(m.attrs.fontSize) + "pt");
+ });
+ }
+
+ const styles = new Map<String, String[]>();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState<any>) {
+ const found = new Set<Mark>();
+ const { from, to } = state.selection as TextSelection;
+ state.doc.nodesBetween(from, to, (node) => node.marks.forEach(m => found.add(m)));
+ return found;
+ }
+
+ //finds all active marks on selection in given group
+ getActiveMarksOnSelection() {
+ if (!this.view) return;
+
+ const markGroup = [schema.marks.strong, schema.marks.em, schema.marks.underline, schema.marks.strikethrough, schema.marks.superscript, schema.marks.subscript];
+ if (this.view.state.storedMarks) return this.view.state.storedMarks.map(mark => mark.type);
+ //current selection
+ const { empty, ranges, $to } = this.view.state.selection as TextSelection;
+ const state = this.view.state;
+ let activeMarks: MarkType[] = [];
+ if (!empty) {
+ activeMarks = markGroup.filter(mark => {
+ const has = false;
+ for (let i = 0; !has && i < ranges.length; i++) {
+ return state.doc.rangeHasMark(ranges[i].$from.pos, ranges[i].$to.pos, mark);
+ }
+ return false;
+ });
+ }
+ else {
+ const pos = this.view.state.selection.$from;
+ const ref_node: ProsNode | null = this.reference_node(pos);
+ if (ref_node !== null && ref_node !== this.view.state.doc) {
+ if (ref_node.isText) {
+ }
+ else {
+ return [];
+ }
+ activeMarks = markGroup.filter(mark_type => {
+ if (mark_type === state.schema.marks.pFontSize) {
+ return ref_node.marks.some(m => m.type.name === state.schema.marks.pFontSize.name);
+ }
+ const mark = state.schema.mark(mark_type);
+ return ref_node.marks.includes(mark);
+ });
+ }
+ }
+ return activeMarks;
+ }
+
+ destroy() {
+ this.fadeOut(true);
+ }
+
+ @action
+ setActiveMarkButtons(activeMarks: MarkType[] | undefined) {
+ if (!activeMarks) return;
+
+ this.boldActive = false;
+ this.italicsActive = false;
+ this.underlineActive = false;
+ this.strikethroughActive = false;
+ this.subscriptActive = false;
+ this.superscriptActive = false;
+
+ activeMarks.forEach(mark => {
+ switch (mark.name) {
+ case "strong": this.boldActive = true; break;
+ case "em": this.italicsActive = true; break;
+ case "underline": this.underlineActive = true; break;
+ case "strikethrough": this.strikethroughActive = true; break;
+ case "subscript": this.subscriptActive = true; break;
+ case "superscript": this.superscriptActive = true; break;
+ }
+ });
+ }
+
+ createButton(faIcon: string, title: string, isActive: boolean = false, command?: any, onclick?: any) {
+ const self = this;
+ function onClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && command && command(self.view.state, self.view.dispatch, self.view);
+ self.view && onclick && onclick(self.view.state, self.view.dispatch, self.view);
+ self.setActiveMarkButtons(self.getActiveMarksOnSelection());
+ }
+
+ return (
+ <button className={"antimodeMenu-button" + (isActive ? " active" : "")} key={title} title={title} onPointerDown={onClick}>
+ <FontAwesomeIcon icon={faIcon as IconProp} size="lg" />
+ </button>
+ );
+ }
+
+ createMarksDropdown(activeOption: string, options: { mark: Mark | null, title: string, label: string, command: (mark: Mark, view: EditorView) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent<HTMLSelectElement>) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ return <select onChange={onChange} key={key}>{items}</select>;
+ }
+
+ createNodesDropdown(activeOption: string, options: { node: NodeType | any | null, title: string, label: string, command: (node: NodeType | any) => void, hidden?: boolean, style?: {} }[], key: string): JSX.Element {
+ const items = options.map(({ title, label, hidden, style }) => {
+ if (hidden) {
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected hidden>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}} hidden>{label}</option>;
+ }
+ return label === activeOption ?
+ <option value={label} title={title} key={label} style={style ? style : {}} selected>{label}</option> :
+ <option value={label} title={title} key={label} style={style ? style : {}}>{label}</option>;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return <select onChange={e => onChange(e.target.value)} key={key}>{items}</select>;
+ }
+
+ changeFontSize = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontSize.create({ fontSize: mark.attrs.fontSize }), view.state, view.dispatch);
+ }
+
+ changeFontFamily = (mark: Mark, view: EditorView) => {
+ this.setMark(view.state.schema.marks.pFontFamily.create({ family: mark.attrs.family }), view.state, view.dispatch);
+ }
+
+ // TODO: remove doesn't work
+ //remove all node type and apply the passed-in one to the selected text
+ changeListType = (nodeType: NodeType | undefined) => {
+ if (!this.view) return;
+
+ if (nodeType === schema.nodes.bullet_list) {
+ wrapInList(nodeType)(this.view.state, this.view.dispatch);
+ } else {
+ const marks = this.view.state.storedMarks || (this.view.state.selection.$to.parentOffset && this.view.state.selection.$from.marks());
+ if (!wrapInList(schema.nodes.ordered_list)(this.view.state, (tx2: any) => {
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view!.dispatch(tx2);
+ })) {
+ const tx2 = this.view.state.tr;
+ const tx3 = updateBullets(tx2, schema, nodeType && (nodeType as any).attrs.mapStyle);
+ marks && tx3.ensureMarks([...marks]);
+ marks && tx3.setStoredMarks([...marks]);
+
+ this.view.dispatch(tx3);
+ }
+ }
+ }
+
+ insertSummarizer(state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ const mark = state.schema.marks.summarize.create();
+ const tr = state.tr;
+ tr.addMark(state.selection.from, state.selection.to, mark);
+ const content = tr.selection.content();
+ const newNode = state.schema.nodes.summary.create({ visibility: false, text: content, textslice: content.toJSON() });
+ dispatch && dispatch(tr.replaceSelectionWith(newNode).removeMark(tr.selection.from - 1, tr.selection.from, mark));
+ return true;
+ }
+
+ @action toggleBrushDropdown() { this.showBrushDropdown = !this.showBrushDropdown; }
+
+ // todo: add brushes to brushMap to save with a style name
+ onBrushNameKeyPress = (e: React.KeyboardEvent) => {
+ if (e.key === "Enter") {
+ RichTextMenu.Instance.brushMarks && RichTextMenu.Instance._brushMap.set(this._brushNameRef.current!.value, RichTextMenu.Instance.brushMarks);
+ this._brushNameRef.current!.style.background = "lightGray";
+ }
+ }
+ _brushNameRef = React.createRef<HTMLInputElement>();
+
+ createBrushButton() {
+ const self = this;
+ function onBrushClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.fillBrush(self.view.state, self.view.dispatch);
+ }
+
+ let label = "Stored marks: ";
+ if (this.brushMarks && this.brushMarks.size > 0) {
+ this.brushMarks.forEach((mark: Mark) => {
+ const markType = mark.type;
+ label += markType.name;
+ label += ", ";
+ });
+ label = label.substring(0, label.length - 2);
+ } else {
+ label = "No marks are currently stored";
+ }
+
+ const button =
+ <button className="antimodeMenu-button" title="" onPointerDown={onBrushClick} style={this.brushMarks?.size > 0 ? { backgroundColor: "121212" } : {}}>
+ <FontAwesomeIcon icon="paint-roller" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.brushMarks?.size > 0 ? 45 : 0}deg)` }} />
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>{label}</p>
+ <button onPointerDown={this.clearBrush}>Clear brush</button>
+ <input placeholder="-brush name-" ref={this._brushNameRef} onKeyPress={this.onBrushNameKeyPress}></input>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"brush dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState<any>, dispatch: any) {
+ if (!this.view) return;
+
+ if (this.brushIsEmpty) {
+ const selected_marks = this.getMarksInSelection(this.view.state);
+ if (selected_marks.size >= 0) {
+ this.brushMarks = selected_marks;
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ else {
+ const { from, to, $from } = this.view.state.selection;
+ if (!this.view.state.selection.empty && $from && $from.nodeAfter) {
+ if (this.brushMarks && to - from > 0) {
+ this.view.dispatch(this.view.state.tr.removeMark(from, to));
+ Array.from(this.brushMarks).filter(m => m.type !== schema.marks.user_mark).forEach((mark: Mark) => {
+ this.setMark(mark, this.view!.state, this.view!.dispatch);
+ });
+ }
+ }
+ else {
+ this.brushIsEmpty = !this.brushIsEmpty;
+ }
+ }
+ }
+
+ @action toggleColorDropdown() { this.showColorDropdown = !this.showColorDropdown; }
+ @action setActiveColor(color: string) { this.activeFontColor = color; }
+
+ createColorButton() {
+ const self = this;
+ function onColorClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+ function changeColor(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveColor(color);
+ self.view && self.insertColor(self.activeFontColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" onPointerDown={onColorClick}>
+ <FontAwesomeIcon icon="palette" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeFontColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown" >
+ <p>Change font color:</p>
+ <div className="color-wrapper">
+ {this.fontColors.map(color => {
+ if (color) {
+ return this.activeFontColor === color ?
+ <button className="color-button active" key={"active" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button> :
+ <button className="color-button" key={"other" + color} style={{ backgroundColor: color }} onPointerDown={e => changeColor(e, color)}></button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"color dropdown"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ public insertColor(color: String, state: EditorState<any>, dispatch: any) {
+ const colorMark = state.schema.mark(state.schema.marks.pFontColor, { color: color });
+ if (state.selection.empty) {
+ dispatch(state.tr.addStoredMark(colorMark));
+ return false;
+ }
+ this.setMark(colorMark, state, dispatch);
+ }
+
+ @action toggleHighlightDropdown() { this.showHighlightDropdown = !this.showHighlightDropdown; }
+ @action setActiveHighlight(color: string) { this.activeHighlightColor = color; }
+
+ createHighlighterButton() {
+ const self = this;
+ function onHighlightClick(e: React.PointerEvent) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+ function changeHighlight(e: React.PointerEvent, color: string) {
+ e.preventDefault();
+ e.stopPropagation();
+ self.view && self.view.focus();
+ self.setActiveHighlight(color);
+ self.view && self.insertHighlight(self.activeHighlightColor, self.view.state, self.view.dispatch);
+ }
+
+ const button =
+ <button className="antimodeMenu-button color-preview-button" title="" key="highilghter-button" onPointerDown={onHighlightClick}>
+ <FontAwesomeIcon icon="highlighter" size="lg" />
+ <div className="color-preview" style={{ backgroundColor: this.activeHighlightColor }}></div>
+ </button>;
+
+ const dropdownContent =
+ <div className="dropdown">
+ <p>Change highlight color:</p>
+ <div className="color-wrapper">
+ {this.highlightColors.map(color => {
+ if (color) {
+ return this.activeHighlightColor === color ?
+ <button className="color-button active" key={`active ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button> :
+ <button className="color-button" key={`inactive ${color}`} style={{ backgroundColor: color }} onPointerDown={e => changeHighlight(e, color)}>{color === "transparent" ? "X" : ""}</button>;
+ }
+ })}
+ </div>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"highlighter"} button={button} dropdownContent={dropdownContent} />
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState<any>, dispatch: any) {
+ if (state.selection.empty) return false;
+ toggleMark(state.schema.marks.marker, { highlight: color })(state, dispatch);
+ }
+
+ @action toggleLinkDropdown() { this.showLinkDropdown = !this.showLinkDropdown; }
+ @action setCurrentLink(link: string) { this.currentLink = link; }
+
+ createLinkButton() {
+ const self = this;
+
+ function onLinkChange(e: React.ChangeEvent<HTMLInputElement>) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = <FontAwesomeIcon icon="link" size="lg" />;
+
+ const dropdownContent =
+ <div className="dropdown link-menu">
+ <p>Linked to:</p>
+ <input value={link} placeholder="Enter URL" onChange={onLinkChange} />
+ <button className="make-button" onPointerDown={e => this.makeLinkToURL(link, "onRight")}>Apply hyperlink</button>
+ <div className="divider"></div>
+ <button className="remove-button" onPointerDown={e => this.deleteLink()}>Remove link</button>
+ </div>;
+
+ return (
+ <ButtonDropdown view={this.view} key={"link button"} button={button} dropdownContent={dropdownContent} openDropdownOnButton={true} />
+ );
+ }
+
+ async getTextLinkTargetTitle() {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type.name === "link");
+ if (link) {
+ const href = link.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ const linkDoc = await DocServer.GetRefField(linkclicked);
+ if (linkDoc instanceof Doc) {
+ const anchor1 = await Cast(linkDoc.anchor1, Doc);
+ const anchor2 = await Cast(linkDoc.anchor2, Doc);
+ const currentDoc = SelectionManager.SelectedDocuments().length && SelectionManager.SelectedDocuments()[0].props.Document;
+ if (currentDoc && anchor1 && anchor2) {
+ if (Doc.AreProtosEqual(currentDoc, anchor1)) {
+ return StrCast(anchor2.title);
+ }
+ if (Doc.AreProtosEqual(currentDoc, anchor2)) {
+ return StrCast(anchor1.title);
+ }
+ }
+ }
+ }
+ } else {
+ return href;
+ }
+ } else {
+ return link.attrs.title;
+ }
+ }
+ }
+
+ // TODO: should check for valid URL
+ makeLinkToURL = (target: String, lcoation: string) => {
+ if (!this.view) return;
+
+ let node = this.view.state.selection.$from.nodeAfter;
+ let link = this.view.state.schema.mark(this.view.state.schema.marks.link, { href: target, location: location });
+ this.view.dispatch(this.view.state.tr.removeMark(this.view.state.selection.from, this.view.state.selection.to, this.view.state.schema.marks.link));
+ this.view.dispatch(this.view.state.tr.addMark(this.view.state.selection.from, this.view.state.selection.to, link));
+ node = this.view.state.selection.$from.nodeAfter;
+ link = node && node.marks.find(m => m.type.name === "link");
+ }
+
+ deleteLink = () => {
+ if (!this.view) return;
+
+ const node = this.view.state.selection.$from.nodeAfter;
+ const link = node && node.marks.find(m => m.type === this.view!.state.schema.marks.link);
+ const href = link!.attrs.href;
+ if (href) {
+ if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ const linkclicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ if (linkclicked) {
+ DocServer.GetRefField(linkclicked).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ LinkManager.Instance.deleteLink(linkDoc);
+ this.view!.dispatch(this.view!.state.tr.removeMark(this.view!.state.selection.from, this.view!.state.selection.to, this.view!.state.schema.marks.link));
+ }
+ });
+ }
+ } else {
+ if (node) {
+ const { tr, schema, selection } = this.view.state;
+ const extension = this.linkExtend(selection.$anchor, href);
+ this.view.dispatch(tr.removeMark(extension.from, extension.to, schema.marks.link));
+ }
+ }
+ }
+ }
+
+ linkExtend($start: ResolvedPos, href: string) {
+ const mark = this.view!.state.schema.marks.link;
+
+ let startIndex = $start.index();
+ let endIndex = $start.indexAfter();
+
+ while (startIndex > 0 && $start.parent.child(startIndex - 1).marks.filter(m => m.type === mark && m.attrs.href === href).length) startIndex--;
+ while (endIndex < $start.parent.childCount && $start.parent.child(endIndex).marks.filter(m => m.type === mark && m.attrs.href === href).length) endIndex++;
+
+ let startPos = $start.start();
+ let endPos = startPos;
+ for (let i = 0; i < endIndex; i++) {
+ const size = $start.parent.child(i).nodeSize;
+ if (i < startIndex) startPos += size;
+ endPos += size;
+ }
+ return { from: startPos, to: endPos };
+ }
+
+ reference_node(pos: ResolvedPos<any>): ProsNode | null {
+ if (!this.view) return null;
+
+ let ref_node: ProsNode = this.view.state.doc;
+ if (pos.nodeBefore !== null && pos.nodeBefore !== undefined) {
+ ref_node = pos.nodeBefore;
+ }
+ else if (pos.nodeAfter !== null && pos.nodeAfter !== undefined) {
+ ref_node = pos.nodeAfter;
+ }
+ else if (pos.pos > 0) {
+ let skip = false;
+ for (let i: number = pos.pos - 1; i > 0; i--) {
+ this.view.state.doc.nodesBetween(i, pos.pos, (node: ProsNode) => {
+ if (node.isLeaf && !skip) {
+ ref_node = node;
+ skip = true;
+ }
+
+ });
+ }
+ }
+ if (!ref_node.isLeaf && ref_node.childCount > 0) {
+ ref_node = ref_node.child(0);
+ }
+ return ref_node;
+ }
+
+ @action onPointerEnter(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = true; }
+ @action onPointerLeave(e: React.PointerEvent) { RichTextMenu.Instance.overMenu = false; }
+
+ @action
+ toggleMenuPin = (e: React.MouseEvent) => {
+ this.Pinned = !this.Pinned;
+ if (!this.Pinned) {
+ this.fadeOut(true);
+ }
+ }
+
+ @action
+ protected toggleCollapse = (e: React.MouseEvent) => {
+ this.collapsed = !this.collapsed;
+ setTimeout(() => {
+ const x = Math.min(this._left, window.innerWidth - RichTextMenu.Instance.width);
+ RichTextMenu.Instance.jumpTo(x, this._top);
+ }, 0);
+ }
+
+ render() {
+
+ const row1 = <div className="antimodeMenu-row" key="row1" style={{ display: this.collapsed ? "none" : undefined }}>{[
+ this.createButton("bold", "Bold", this.boldActive, toggleMark(schema.marks.strong)),
+ this.createButton("italic", "Italic", this.italicsActive, toggleMark(schema.marks.em)),
+ this.createButton("underline", "Underline", this.underlineActive, toggleMark(schema.marks.underline)),
+ this.createButton("strikethrough", "Strikethrough", this.strikethroughActive, toggleMark(schema.marks.strikethrough)),
+ this.createButton("superscript", "Superscript", this.superscriptActive, toggleMark(schema.marks.superscript)),
+ this.createButton("subscript", "Subscript", this.subscriptActive, toggleMark(schema.marks.subscript)),
+ this.createColorButton(),
+ this.createHighlighterButton(),
+ this.createLinkButton(),
+ this.createBrushButton(),
+ this.createButton("indent", "Summarize", undefined, this.insertSummarizer),
+ ]}</div>;
+
+ const row2 = <div className="antimodeMenu-row row-2" key="antimodemenu row2">
+ <div key="row" style={{ display: this.collapsed ? "none" : undefined }}>
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
+ </div>
+ <div key="button">
+ <div key="collapser">
+ <button className="antimodeMenu-button" key="collapse menu" title="Collapse menu" onClick={this.toggleCollapse} style={{ backgroundColor: this.collapsed ? "#121212" : "", width: 25 }}>
+ <FontAwesomeIcon icon="chevron-left" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.3s", transform: `rotate(${this.collapsed ? 180 : 0}deg)` }} />
+ </button>
+ </div>
+ <button className="antimodeMenu-button" key="pin menu" title="Pin menu" onClick={this.toggleMenuPin} style={{ backgroundColor: this.Pinned ? "#121212" : "", display: this.collapsed ? "none" : undefined }}>
+ <FontAwesomeIcon icon="thumbtack" size="lg" style={{ transitionProperty: "transform", transitionDuration: "0.1s", transform: `rotate(${this.Pinned ? 45 : 0}deg)` }} />
+ </button>
+ {this.getDragger()}
+ </div>
+ </div>;
+
+ return (
+ <div className="richTextMenu" onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave}>
+ {this.getElementWithRows([row1, row2], 2, false)}
+ </div>
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ view?: EditorView;
+ button: JSX.Element;
+ dropdownContent: JSX.Element;
+ openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component<ButtonDropdownProps> {
+
+ @observable private showDropdown: boolean = false;
+ private ref: HTMLDivElement | null = null;
+
+ componentDidMount() {
+ document.addEventListener("pointerdown", this.onBlur);
+ }
+
+ componentWillUnmount() {
+ document.removeEventListener("pointerdown", this.onBlur);
+ }
+
+ @action
+ setShowDropdown(show: boolean) {
+ this.showDropdown = show;
+ }
+ @action
+ toggleDropdown() {
+ this.showDropdown = !this.showDropdown;
+ }
+
+ onDropdownClick = (e: React.PointerEvent) => {
+ e.preventDefault();
+ e.stopPropagation();
+ this.props.view && this.props.view.focus();
+ this.toggleDropdown();
+ }
+
+ onBlur = (e: PointerEvent) => {
+ setTimeout(() => {
+ if (this.ref !== null && !this.ref.contains(e.target as Node)) {
+ this.setShowDropdown(false);
+ }
+ }, 0);
+ }
+
+ render() {
+ return (
+ <div className="button-dropdown-wrapper" ref={node => this.ref = node}>
+ {this.props.openDropdownOnButton ?
+ <button className="antimodeMenu-button dropdown-button-combined" onPointerDown={this.onDropdownClick}>
+ {this.props.button}
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button> :
+ <>
+ {this.props.button}
+ <button className="dropdown-button antimodeMenu-button" key="antimodebutton" onPointerDown={this.onDropdownClick}>
+ <FontAwesomeIcon icon="caret-down" size="sm" />
+ </button>
+ </>}
+
+ {this.showDropdown ? this.props.dropdownContent : (null)}
+ </div>
+ );
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts
new file mode 100644
index 000000000..335094e23
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextRules.ts
@@ -0,0 +1,319 @@
+import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from "prosemirror-inputrules";
+import { NodeSelection, TextSelection } from "prosemirror-state";
+import { DataSym, Doc } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { Cast, NumCast } from "../../../../new_fields/Types";
+import { returnFalse, Utils } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs, DocUtils } from "../../../documents/Documents";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { wrappingInputRule } from "./prosemirrorPatches";
+import RichTextMenu from "./RichTextMenu";
+import { schema } from "./schema_rts";
+
+export class RichTextRules {
+ public Document: Doc;
+ public TextBox: FormattedTextBox;
+ public EnteringStyle: boolean = false;
+ constructor(doc: Doc, textBox: FormattedTextBox) {
+ this.Document = doc;
+ this.TextBox = textBox;
+ }
+ public inpRules = {
+ rules: [
+ ...smartQuotes,
+ ellipsis,
+ emDash,
+
+ // > blockquote
+ wrappingInputRule(/^\s*>\s$/, schema.nodes.blockquote),
+
+ // 1. ordered list
+ wrappingInputRule(
+ /^1\.\s$/,
+ schema.nodes.ordered_list,
+ () => {
+ return ({ mapStyle: "decimal", bulletStyle: 1 });
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "decimal", bulletStyle: 1 } })
+ ),
+ // a. alphabbetical list
+ wrappingInputRule(
+ /^a\.\s$/,
+ schema.nodes.ordered_list,
+ // match => {
+ () => {
+ return ({ mapStyle: "alpha", bulletStyle: 1 });
+ // return ({ order: +match[1] })
+ },
+ (match: any, node: any) => {
+ return node.childCount + node.attrs.order === +match[1];
+ },
+ (type: any) => ({ type: type, attrs: { mapStyle: "alpha", bulletStyle: 1 } })
+ ),
+
+ // * bullet list
+ wrappingInputRule(/^\s*([-+*])\s$/, schema.nodes.bullet_list),
+
+ // ``` code block
+ textblockTypeInputRule(/^```$/, schema.nodes.code_block),
+
+ // create an inline view of a tag stored under the '#' field
+ new InputRule(
+ new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/),
+ (state, match, start, end) => {
+ const tag = match[1];
+ if (!tag) return state.tr;
+ this.Document[DataSym]["#"] = tag;
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey: "#" });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+
+ // # heading
+ textblockTypeInputRule(
+ new RegExp(/^(#{1,6})\s$/),
+ schema.nodes.heading,
+ match => {
+ return ({ level: match[1].length });
+ }
+ ),
+
+ // set the font size using #<font-size>
+ new InputRule(
+ new RegExp(/%([0-9]+)\s$/),
+ (state, match, start, end) => {
+ const size = Number(match[1]);
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontSize.create({ fontSize: size }));
+ }),
+
+ // create a text display of a metadata field on this or another document, or create a hyperlink portal to another document [[ <fieldKey> : <Doc>]] // [[:Doc]] => hyperlink [[fieldKey]] => show field [[fieldKey:Doc]] => show field of doc
+ new InputRule(
+ new RegExp(/\[\[([a-zA-Z_@\? \-0-9]*)(=[a-zA-Z_@\? \-0-9]*)?(:[a-zA-Z_@\? \-0-9]+)?\]\]$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1];
+ const docid = match[3]?.substring(1);
+ const value = match[2]?.substring(1);
+ if (!fieldKey) {
+ if (docid) {
+ DocServer.GetRefField(docid).then(docx => {
+ const target = ((docx instanceof Doc) && docx) || Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true, }, docid);
+ DocUtils.Publish(target, docid, returnFalse, returnFalse);
+ DocUtils.MakeLink({ doc: this.Document }, { doc: target }, "portal to");
+ });
+ const link = state.schema.marks.link.create({ href: Utils.prepend("/doc/" + docid), location: "onRight", title: docid, targetId: docid });
+ return state.tr.deleteRange(end - 1, end).deleteRange(start, start + 2).addMark(start, end - 3, link);
+ }
+ return state.tr;
+ }
+ if (value !== "" && value !== undefined) {
+ const num = value.match(/^[0-9.]/);
+ this.Document[DataSym][fieldKey] = value === "true" ? true : value === "false" ? false : (num ? Number(value) : value);
+ }
+ const fieldView = state.schema.nodes.dashField.create({ fieldKey, docid });
+ return state.tr.deleteRange(start, end).insert(start, fieldView);
+ }),
+ // create an inline view of a document {{ <layoutKey> : <Doc> }} // {{:Doc}} => show default view of document {{<layout>}} => show layout for this doc {{<layout> : Doc}} => show layout for another doc
+ new InputRule(
+ new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._\-]*\))?(:[a-zA-Z_ \-0-9]+)?\}\}$/),
+ (state, match, start, end) => {
+ const fieldKey = match[1] || "";
+ const fieldParam = match[2]?.replace("…", "...") || "";
+ const docid = match[3]?.substring(1);
+ if (!fieldKey && !docid) return state.tr;
+ docid && DocServer.GetRefField(docid).then(docx => {
+ if (!(docx instanceof Doc && docx)) {
+ const docx = Docs.Create.FreeformDocument([], { title: docid, _width: 500, _height: 500, _LODdisable: true }, docid);
+ DocUtils.Publish(docx, docid, returnFalse, returnFalse);
+ }
+ });
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: "dashDoc", docid, fieldKey: fieldKey + fieldParam, float: "unset", alias: Utils.GenerateGuid() });
+ const sm = state.storedMarks || undefined;
+ return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr;
+ }),
+ new InputRule(
+ new RegExp(/>>$/),
+ (state, match, start, end) => {
+ const textDoc = this.Document[DataSym];
+ const numInlines = NumCast(textDoc.inlineTextCount);
+ textDoc.inlineTextCount = numInlines + 1;
+ const inlineFieldKey = "inline" + numInlines; // which field on the text document this annotation will write to
+ const inlineLayoutKey = "layout_" + inlineFieldKey; // the field holding the layout string that will render the inline annotation
+ const textDocInline = Docs.Create.TextDocument("", { layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _autoHeight: true, _fontSize: 9, title: "inline comment" });
+ textDocInline.title = inlineFieldKey; // give the annotation its own title
+ textDocInline.customTitle = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc
+ textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point
+ textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]]
+ textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`);
+ textDoc[inlineLayoutKey] = FormattedTextBox.LayoutString(inlineFieldKey); // create a layout string for the layout key that will render the annotation text
+ textDoc[inlineFieldKey] = ""; // set a default value for the annotation
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const newNode = schema.nodes.dashComment.create({ docid: textDocInline[Id] });
+ const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: textDocInline[Id], float: "right" });
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.insert(start, newNode).replaceRangeWith(start + 1, end + 1, dashDoc).insertText(" ", start + 2).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced;
+ }),
+ // stop using active style
+ new InputRule(
+ new RegExp(/%%$/),
+ (state, match, start, end) => {
+ const tr = state.tr.deleteRange(start, end);
+ const marks = state.tr.selection.$anchor.nodeBefore?.marks;
+ return marks ? Array.from(marks).filter(m => m !== state.schema.marks.user_mark).reduce((tr, m) => tr.removeStoredMark(m), tr) : tr;
+ }),
+
+ // set the Todo user-tag on the current selection (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/[ti!x]$/),
+ (state, match, start, end) => {
+ if (state.selection.to === state.selection.from || !this.EnteringStyle) return null;
+ const tag = match[0] === "t" ? "todo" : match[0] === "i" ? "ignore" : match[0] === "x" ? "disagree" : match[0] === "!" ? "important" : "??";
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ if (node?.marks.findIndex((m: any) => m.type === schema.marks.user_tag) !== -1) return state.tr.removeMark(start, end, schema.marks.user_tag);
+ return node ? state.tr.addMark(start, end, schema.marks.user_tag.create({ userid: Doc.CurrentUserEmail, tag: tag, modified: Math.round(Date.now() / 1000 / 60) })) : state.tr;
+ }),
+
+ // set the First-line indent node type for the selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%d|d)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === 25 ? undefined : 25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+
+ // set the Hanging indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%h|h)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, indent: node.attrs.indent === -25 ? undefined : -25 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+ // set the Quoted indent node type for the current selection's paragraph (assumes % was used to initiate an EnteringStyle mode)
+ new InputRule(
+ new RegExp(/(%q|q)$/),
+ (state, match, start, end) => {
+ if (!match[0].startsWith("%") && !this.EnteringStyle) return null;
+ const pos = (state.doc.resolve(start) as any);
+ if (state.selection instanceof NodeSelection && state.selection.node.type === schema.nodes.ordered_list) {
+ const node = state.selection.node;
+ return state.tr.setNodeMarkup(pos.pos, node.type, { ...node.attrs, indent: node.attrs.indent === 30 ? undefined : 30 });
+ }
+ for (let depth = pos.path.length / 3 - 1; depth >= 0; depth--) {
+ const node = pos.node(depth);
+ if (node.type === schema.nodes.paragraph) {
+ const replaced = state.tr.setNodeMarkup(pos.pos - pos.parentOffset - 1, node.type, { ...node.attrs, inset: node.attrs.inset === 30 ? undefined : 30 });
+ const result = replaced.setSelection(new TextSelection(replaced.doc.resolve(start)));
+ return match[0].startsWith("%") ? result.deleteRange(start, end) : result;
+ }
+ }
+ return null;
+ }),
+
+
+ // center justify text
+ new InputRule(
+ new RegExp(/%\^$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "center" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // left justify text
+ new InputRule(
+ new RegExp(/%\[$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "left" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ // right justify text
+ new InputRule(
+ new RegExp(/%\]$/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || undefined;
+ const replaced = node ? state.tr.replaceRangeWith(start, end, schema.nodes.paragraph.create({ align: "right" })).setStoredMarks([...node.marks, ...(sm ? sm : [])]) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end - 2)));
+ }),
+ new InputRule(
+ new RegExp(/%\(/),
+ (state, match, start, end) => {
+ const node = (state.doc.resolve(start) as any).nodeAfter;
+ const sm = state.storedMarks || [];
+ const mark = state.schema.marks.summarizeInclusive.create();
+ sm.push(mark);
+ const selected = state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).addMark(start, end, mark);
+ const content = selected.selection.content();
+ const replaced = node ? selected.replaceRangeWith(start, end,
+ schema.nodes.summary.create({ visibility: true, text: content, textslice: content.toJSON() })) :
+ state.tr;
+ return replaced.setSelection(new TextSelection(replaced.doc.resolve(end + 1))).setStoredMarks([...node.marks, ...sm]);
+ }),
+ new InputRule(
+ new RegExp(/%\)/),
+ (state, match, start, end) => {
+ return state.tr.deleteRange(start, end).removeStoredMark(state.schema.marks.summarizeInclusive.create());
+ }),
+ new InputRule(
+ new RegExp(/%f$/),
+ (state, match, start, end) => {
+ const newNode = schema.nodes.footnote.create({});
+ const tr = state.tr;
+ tr.deleteRange(start, end).replaceSelectionWith(newNode); // replace insertion with a footnote.
+ return tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize)));
+ }),
+
+ // activate a style by name using prefix '%'
+ new InputRule(
+ new RegExp(/%[a-z]+$/),
+ (state, match, start, end) => {
+ const color = match[0].substring(1, match[0].length);
+ const marks = RichTextMenu.Instance._brushMap.get(color);
+ if (marks) {
+ const tr = state.tr.deleteRange(start, end);
+ return marks ? Array.from(marks).reduce((tr, m) => tr.addStoredMark(m), tr) : tr;
+ }
+ const isValidColor = (strColor: string) => {
+ const s = new Option().style;
+ s.color = strColor;
+ return s.color === strColor.toLowerCase(); // 'false' if color wasn't assigned
+ };
+ if (isValidColor(color)) {
+ return state.tr.deleteRange(start, end).addStoredMark(schema.marks.pFontColor.create({ color: color }));
+ }
+ return null;
+ }),
+ ]
+ };
+}
diff --git a/src/client/views/nodes/formattedText/RichTextSchema.tsx b/src/client/views/nodes/formattedText/RichTextSchema.tsx
new file mode 100644
index 000000000..33caf5751
--- /dev/null
+++ b/src/client/views/nodes/formattedText/RichTextSchema.tsx
@@ -0,0 +1,718 @@
+import { IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { baseKeymap, toggleMark } from "prosemirror-commands";
+import { redo, undo } from "prosemirror-history";
+import { keymap } from "prosemirror-keymap";
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import { EditorState, NodeSelection, Plugin, TextSelection } from "prosemirror-state";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import * as ReactDOM from 'react-dom';
+import { Doc, DocListCast, Field, HeightSym, WidthSym } from "../../../../new_fields/Doc";
+import { Id } from "../../../../new_fields/FieldSymbols";
+import { List } from "../../../../new_fields/List";
+import { ObjectField } from "../../../../new_fields/ObjectField";
+import { listSpec } from "../../../../new_fields/Schema";
+import { SchemaHeaderField } from "../../../../new_fields/SchemaHeaderField";
+import { ComputedField } from "../../../../new_fields/ScriptField";
+import { BoolCast, Cast, NumCast, StrCast, FieldValue } from "../../../../new_fields/Types";
+import { emptyFunction, returnEmptyString, returnFalse, returnOne, Utils, returnZero } from "../../../../Utils";
+import { DocServer } from "../../../DocServer";
+import { Docs } from "../../../documents/Documents";
+import { CollectionViewType } from "../../collections/CollectionView";
+import { DocumentView } from "../DocumentView";
+import { FormattedTextBox } from "./FormattedTextBox";
+import { DocumentManager } from "../../../util/DocumentManager";
+import { Transform } from "../../../util/Transform";
+import React = require("react");
+
+import { schema } from "./schema_rts";
+
+export class OrderedListView {
+ update(node: any) {
+ return false; // if attr's of an ordered_list (e.g., bulletStyle) change, return false forces the dom node to be recreated which is necessary for the bullet labels to update
+ }
+}
+
+export class ImageResizeView {
+ _handle: HTMLElement;
+ _img: HTMLElement;
+ _outer: HTMLElement;
+ constructor(node: any, view: any, getPos: any, addDocTab: any) {
+ //moved
+ this._handle = document.createElement("span");
+ this._img = document.createElement("img");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = "inline-block";
+ this._outer.style.overflow = "hidden";
+ (this._outer.style as any).float = node.attrs.float;
+ //moved
+ this._img.setAttribute("src", node.attrs.src);
+ this._img.style.width = "100%";
+ this._handle.style.position = "absolute";
+ this._handle.style.width = "20px";
+ this._handle.style.height = "20px";
+ this._handle.style.backgroundColor = "blue";
+ this._handle.style.borderRadius = "15px";
+ this._handle.style.display = "none";
+ this._handle.style.bottom = "-10px";
+ this._handle.style.right = "-10px";
+ const self = this;
+ //moved
+ this._img.onclick = function (e: any) {
+ e.stopPropagation();
+ e.preventDefault();
+ if (view.state.selection.node && view.state.selection.node.type !== view.state.schema.nodes.image) {
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(view.state.selection.from - 2))));
+ }
+ };
+ //moved
+ this._img.onpointerdown = function (e: any) {
+ if (e.ctrlKey) {
+ e.preventDefault();
+ e.stopPropagation();
+ DocServer.GetRefField(node.attrs.docid).then(async linkDoc =>
+ (linkDoc instanceof Doc) &&
+ DocumentManager.Instance.FollowLink(linkDoc, view.state.schema.Document,
+ document => addDocTab(document, node.attrs.location ? node.attrs.location : "inTab"), false));
+ }
+ };
+ //moved
+ this._handle.onpointerdown = function (e: any) {
+ e.preventDefault();
+ e.stopPropagation();
+ const wid = Number(getComputedStyle(self._img).width.replace(/px/, ""));
+ const hgt = Number(getComputedStyle(self._img).height.replace(/px/, ""));
+ const startX = e.pageX;
+ const startWidth = parseFloat(node.attrs.width);
+ const onpointermove = (e: any) => {
+ const currentX = e.pageX;
+ const diffInPx = currentX - startX;
+ self._outer.style.width = `${startWidth + diffInPx}`;
+ self._outer.style.height = `${(startWidth + diffInPx) * hgt / wid}`;
+ };
+
+ const onpointerup = () => {
+ document.removeEventListener("pointermove", onpointermove);
+ document.removeEventListener("pointerup", onpointerup);
+ const pos = view.state.selection.from;
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: self._outer.style.width, height: self._outer.style.height }));
+ view.dispatch(view.state.tr.setSelection(new NodeSelection(view.state.doc.resolve(pos))));
+ };
+
+ document.addEventListener("pointermove", onpointermove);
+ document.addEventListener("pointerup", onpointerup);
+ };
+ //Moved
+ this._outer.appendChild(this._img);
+ this._outer.appendChild(this._handle);
+ (this as any).dom = this._outer;
+ }
+
+ selectNode() {
+ this._img.classList.add("ProseMirror-selectednode");
+
+ this._handle.style.display = "";
+ }
+
+ deselectNode() {
+ this._img.classList.remove("ProseMirror-selectednode");
+
+ this._handle.style.display = "none";
+ }
+}
+
+export class DashDocCommentView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ //moved
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = "formattedTextBox-inlineComment";
+ this._collapsed.id = "DashDocCommentView-" + node.attrs.docid;
+ this._view = view;
+ //moved
+ const targetNode = () => { // search forward in the prosemirror doc for the attached dashDocNode that is the target of the comment anchor
+ for (let i = getPos() + 1; i < view.state.doc.content.size; i++) {
+ const m = view.state.doc.nodeAt(i);
+ if (m && m.type === view.state.schema.nodes.dashDoc && m.attrs.docid === node.attrs.docid) {
+ return { node: m, pos: i, hidden: m.attrs.hidden } as { node: any, pos: number, hidden: boolean };
+ }
+ }
+ const dashDoc = view.state.schema.nodes.dashDoc.create({ width: 75, height: 35, title: "dashDoc", docid: node.attrs.docid, float: "right" });
+ view.dispatch(view.state.tr.insert(getPos() + 1, dashDoc));
+ setTimeout(() => { try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + 2))); } catch (e) { } }, 0);
+ return undefined;
+ };
+ //moved
+ this._collapsed.onpointerdown = (e: any) => {
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerup = (e: any) => {
+ const target = targetNode();
+ if (target) {
+ const expand = target.hidden;
+ const tr = view.state.tr.setNodeMarkup(target.pos, undefined, { ...target.node.attrs, hidden: target.node.attrs.hidden ? false : true });
+ view.dispatch(tr.setSelection(TextSelection.create(tr.doc, getPos() + (expand ? 2 : 1)))); // update the attrs
+ setTimeout(() => {
+ expand && DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc));
+ try { view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.tr.doc, getPos() + (expand ? 2 : 1)))); } catch (e) { }
+ }, 0);
+ }
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerenter = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowHighlight(dashDoc, false));
+ e.preventDefault();
+ e.stopPropagation();
+ };
+ //moved
+ this._collapsed.onpointerleave = (e: any) => {
+ DocServer.GetRefField(node.attrs.docid).then(async dashDoc => dashDoc instanceof Doc && Doc.linkFollowUnhighlight());
+ e.preventDefault();
+ e.stopPropagation();
+ };
+
+ (this as any).dom = this._collapsed;
+ }
+ //moved
+ selectNode() { }
+}
+
+export class DashDocView {
+ _dashSpan: HTMLDivElement;
+ _outer: HTMLElement;
+ _dashDoc: Doc | undefined;
+ _reactionDisposer: IReactionDisposer | undefined;
+ _renderDisposer: IReactionDisposer | undefined;
+ _textBox: FormattedTextBox;
+
+ getDocTransform = () => {
+ const { scale, translateX, translateY } = Utils.GetScreenTransform(this._outer);
+ return new Transform(-translateX, -translateY, 1).scale(1 / this.contentScaling() / scale);
+ }
+ contentScaling = () => NumCast(this._dashDoc!._nativeWidth) > 0 ? this._dashDoc![WidthSym]() / NumCast(this._dashDoc!._nativeWidth) : 1;
+
+ outerFocus = (target: Doc) => this._textBox.props.focus(this._textBox.props.Document); // ideally, this would scroll to show the focus target
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._textBox = tbox;
+ this._dashSpan = document.createElement("div");
+ this._outer = document.createElement("span");
+ this._outer.style.position = "relative";
+ this._outer.style.textIndent = "0";
+ this._outer.style.border = "1px solid " + StrCast(tbox.layoutDoc.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ this._outer.style.width = node.attrs.width;
+ this._outer.style.height = node.attrs.height;
+ this._outer.style.display = node.attrs.hidden ? "none" : "inline-block";
+ // this._outer.style.overflow = "hidden"; // bcz: not sure if this is needed. if it's used, then the doc doesn't highlight when you hover over a docComment
+ (this._outer.style as any).float = node.attrs.float;
+
+ this._dashSpan.style.width = node.attrs.width;
+ this._dashSpan.style.height = node.attrs.height;
+ this._dashSpan.style.position = "absolute";
+ this._dashSpan.style.display = "inline-block";
+ this._dashSpan.onpointerleave = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "";
+ }
+ };
+ this._dashSpan.onpointerenter = () => {
+ const ele = document.getElementById("DashDocCommentView-" + node.attrs.docid);
+ if (ele) {
+ (ele as HTMLDivElement).style.backgroundColor = "orange";
+ }
+ };
+ const removeDoc = () => {
+ const pos = getPos();
+ const ns = new NodeSelection(view.state.doc.resolve(pos));
+ view.dispatch(view.state.tr.setSelection(ns).deleteSelection());
+ return true;
+ };
+ const alias = node.attrs.alias;
+
+ const docid = node.attrs.docid || tbox.props.Document[Id];// tbox.props.DataDoc?.[Id] || tbox.dataDoc?.[Id];
+ DocServer.GetRefField(docid + alias).then(async dashDoc => {
+ if (!(dashDoc instanceof Doc)) {
+ alias && DocServer.GetRefField(docid).then(async dashDocBase => {
+ if (dashDocBase instanceof Doc) {
+ const aliasedDoc = Doc.MakeAlias(dashDocBase, docid + alias);
+ aliasedDoc.layoutKey = "layout";
+ node.attrs.fieldKey && Doc.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, node.attrs.fieldKey, undefined);
+ self.doRender(aliasedDoc, removeDoc, node, view, getPos);
+ }
+ });
+ } else {
+ self.doRender(dashDoc, removeDoc, node, view, getPos);
+ }
+ });
+ const self = this;
+ this._dashSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ };
+ this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._dashSpan.onwheel = function (e: any) { e.preventDefault(); };
+ this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._outer.appendChild(this._dashSpan);
+ (this as any).dom = this._outer;
+ }
+
+ doRender(dashDoc: Doc, removeDoc: any, node: any, view: any, getPos: any) {
+ this._dashDoc = dashDoc;
+ const self = this;
+ const dashLayoutDoc = Doc.Layout(dashDoc);
+ const finalLayout = node.attrs.docid ? dashDoc : Doc.expandTemplateLayout(dashLayoutDoc, dashDoc, node.attrs.fieldKey);
+
+ if (!finalLayout) setTimeout(() => self.doRender(dashDoc, removeDoc, node, view, getPos), 0);
+ else {
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => ({ dim: [finalLayout[WidthSym](), finalLayout[HeightSym]()], color: finalLayout.color }), ({ dim, color }) => {
+ this._dashSpan.style.width = this._outer.style.width = Math.max(20, dim[0]) + "px";
+ this._dashSpan.style.height = this._outer.style.height = Math.max(20, dim[1]) + "px";
+ this._outer.style.border = "1px solid " + StrCast(finalLayout.color, (Cast(Doc.UserDoc().activeWorkspace, Doc, null).darkScheme ? "dimGray" : "lightGray"));
+ }, { fireImmediately: true });
+ const doReactRender = (finalLayout: Doc, resolvedDataDoc: Doc) => {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+
+ ReactDOM.render(<DocumentView
+ Document={finalLayout}
+ DataDoc={resolvedDataDoc}
+ LibraryPath={this._textBox.props.LibraryPath}
+ fitToBox={BoolCast(dashDoc._fitToBox)}
+ addDocument={returnFalse}
+ rootSelected={this._textBox.props.isSelected}
+ removeDocument={removeDoc}
+ ScreenToLocalTransform={this.getDocTransform}
+ addDocTab={this._textBox.props.addDocTab}
+ pinToPres={returnFalse}
+ renderDepth={self._textBox.props.renderDepth + 1}
+ NativeHeight={returnZero}
+ NativeWidth={returnZero}
+ PanelWidth={finalLayout[WidthSym]}
+ PanelHeight={finalLayout[HeightSym]}
+ focus={this.outerFocus}
+ backgroundColor={returnEmptyString}
+ parentActive={returnFalse}
+ whenActiveChanged={returnFalse}
+ bringToFront={emptyFunction}
+ dontRegisterView={false}
+ ContainingCollectionView={this._textBox.props.ContainingCollectionView}
+ ContainingCollectionDoc={this._textBox.props.ContainingCollectionDoc}
+ ContentScaling={this.contentScaling}
+ />, this._dashSpan);
+ if (node.attrs.width !== dashDoc._width + "px" || node.attrs.height !== dashDoc._height + "px") {
+ try { // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made
+ view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, width: dashDoc._width + "px", height: dashDoc._height + "px" }));
+ } catch (e) {
+ console.log(e);
+ }
+ }
+ };
+ this._renderDisposer?.();
+ this._renderDisposer = reaction(() => {
+ // if (!Doc.AreProtosEqual(finalLayout, dashDoc)) {
+ // finalLayout.rootDocument = dashDoc.aliasOf; // bcz: check on this ... why is it here?
+ // }
+ const layoutKey = StrCast(finalLayout.layoutKey);
+ const finalKey = layoutKey && StrCast(finalLayout[layoutKey]).split("'")?.[1];
+ if (finalLayout !== dashDoc && finalKey) {
+ const finalLayoutField = finalLayout[finalKey];
+ if (finalLayoutField instanceof ObjectField) {
+ finalLayout[finalKey + "-textTemplate"] = ComputedField.MakeFunction(`copyField(this.${finalKey})`, { this: Doc.name });
+ }
+ }
+ return { finalLayout, resolvedDataDoc: Cast(finalLayout.resolvedDataDoc, Doc, null) };
+ },
+ (res) => doReactRender(res.finalLayout, res.resolvedDataDoc),
+ { fireImmediately: true });
+ }
+ }
+ destroy() {
+ ReactDOM.unmountComponentAtNode(this._dashSpan);
+ this._reactionDisposer?.();
+ }
+}
+
+export class DashFieldView {
+ _fieldWrapper: HTMLDivElement; // container for label and value
+ _labelSpan: HTMLSpanElement; // field label
+ _fieldSpan: HTMLSpanElement; // field value
+ _fieldCheck: HTMLInputElement;
+ _enumerables: HTMLDivElement; // field value
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ @observable _dashDoc: Doc | undefined;
+ _fieldKey: string;
+ _options: Doc[] = [];
+
+ constructor(node: any, view: any, getPos: any, tbox: FormattedTextBox) {
+ this._fieldKey = node.attrs.fieldKey;
+ this._textBoxDoc = tbox.props.Document;
+ this._fieldWrapper = document.createElement("p");
+ this._fieldWrapper.style.width = node.attrs.width;
+ this._fieldWrapper.style.height = node.attrs.height;
+ this._fieldWrapper.style.fontWeight = "bold";
+ this._fieldWrapper.style.position = "relative";
+ this._fieldWrapper.style.display = "inline-block";
+
+ const self = this;
+
+ this._enumerables = document.createElement("div");
+ this._enumerables.style.width = "10px";
+ this._enumerables.style.height = "10px";
+ this._enumerables.style.position = "relative";
+ this._enumerables.style.display = "none";
+
+ //Moved
+ this._enumerables.onpointerdown = async (e) => {
+ e.stopPropagation();
+ const collview = await Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ collview instanceof Doc && tbox.props.addDocTab(collview, "onRight");
+ };
+ //Moved
+ const updateText = (forceMatch: boolean) => {
+ self._enumerables.style.display = "none";
+ const newText = self._fieldSpan.innerText.startsWith(":=") || self._fieldSpan.innerText.startsWith("=:=") ? ":=-computed-" : self._fieldSpan.innerText;
+
+ // look for a document whose id === the fieldKey being displayed. If there's a match, then that document
+ // holds the different enumerated values for the field in the titles of its collected documents.
+ // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down.
+ DocServer.GetRefField(self._fieldKey).then(options => {
+ let modText = "";
+ (options instanceof Doc) && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title)));
+ if (modText) {
+ self._fieldSpan.innerHTML = self._dashDoc![self._fieldKey] = modText;
+ Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, []);
+ } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key
+ else if (self._fieldSpan.innerText.startsWith(":=")) {
+ self._dashDoc![self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(2));
+ } else if (self._fieldSpan.innerText.startsWith("=:=")) {
+ Doc.Layout(tbox.props.Document)[self._fieldKey] = ComputedField.MakeFunction(self._fieldSpan.innerText.substring(3));
+ } else {
+ self._dashDoc![self._fieldKey] = newText;
+ }
+ });
+ };
+
+ //Moved
+ this._fieldCheck = document.createElement("input");
+ this._fieldCheck.id = Utils.GenerateGuid();
+ this._fieldCheck.type = "checkbox";
+ this._fieldCheck.style.position = "relative";
+ this._fieldCheck.style.display = "none";
+ this._fieldCheck.style.minWidth = "12px";
+ this._fieldCheck.style.backgroundColor = "rgba(155, 155, 155, 0.24)";
+ this._fieldCheck.onchange = function (e: any) {
+ self._dashDoc![self._fieldKey] = e.target.checked;
+ };
+
+ this._fieldSpan = document.createElement("span");
+ this._fieldSpan.id = Utils.GenerateGuid();
+ this._fieldSpan.contentEditable = "true";
+ this._fieldSpan.style.position = "relative";
+ this._fieldSpan.style.display = "none";
+ this._fieldSpan.style.minWidth = "12px";
+ this._fieldSpan.style.fontSize = "large";
+ this._fieldSpan.onkeypress = function (e: any) { e.stopPropagation(); };
+ this._fieldSpan.onkeyup = function (e: any) { e.stopPropagation(); };
+ this._fieldSpan.onmousedown = function (e: any) { e.stopPropagation(); self._enumerables.style.display = "inline-block"; };
+ this._fieldSpan.onblur = function (e: any) { updateText(false); };
+
+ // MOVED
+ const setDashDoc = (doc: Doc) => {
+ self._dashDoc = doc;
+ if (self._options?.length && !self._dashDoc[self._fieldKey]) {
+ self._dashDoc[self._fieldKey] = StrCast(self._options[0].title);
+ }
+ this._labelSpan.innerHTML = `${self._fieldKey}: `;
+ const fieldVal = Cast(this._dashDoc?.[self._fieldKey], "boolean", null);
+ this._fieldCheck.style.display = (fieldVal === true || fieldVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(fieldVal === true || fieldVal === false) ? StrCast(this._dashDoc?.[self._fieldKey]) ? "" : "inline-block" : "none";
+ };
+
+ //Moved
+ this._fieldSpan.onkeydown = function (e: any) {
+ e.stopPropagation();
+ if ((e.key === "a" && e.ctrlKey) || (e.key === "a" && e.metaKey)) {
+ if (window.getSelection) {
+ const range = document.createRange();
+ range.selectNodeContents(self._fieldSpan);
+ window.getSelection()!.removeAllRanges();
+ window.getSelection()!.addRange(range);
+ }
+ e.preventDefault();
+ }
+ if (e.key === "Enter") {
+ e.preventDefault();
+ e.ctrlKey && Doc.addFieldEnumerations(self._textBoxDoc, self._fieldKey, [{ title: self._fieldSpan.innerText }]);
+ updateText(true);
+ }
+ };
+
+ this._labelSpan = document.createElement("span");
+ this._labelSpan.style.position = "relative";
+ this._labelSpan.style.fontSize = "small";
+ this._labelSpan.title = "click to see related tags";
+ this._labelSpan.style.fontSize = "x-small";
+ this._labelSpan.onpointerdown = function (e: any) {
+ e.stopPropagation();
+ let container = tbox.props.ContainingCollectionView;
+ while (container?.props.Document.isTemplateForField || container?.props.Document.isTemplateDoc) {
+ container = container.props.ContainingCollectionView;
+ }
+ if (container) {
+ const alias = Doc.MakeAlias(container.props.Document);
+ alias.viewType = CollectionViewType.Time;
+ let list = Cast(alias.schemaColumns, listSpec(SchemaHeaderField));
+ if (!list) {
+ alias.schemaColumns = list = new List<SchemaHeaderField>();
+ }
+ list.map(c => c.heading).indexOf(self._fieldKey) === -1 && list.push(new SchemaHeaderField(self._fieldKey, "#f1efeb"));
+ list.map(c => c.heading).indexOf("text") === -1 && list.push(new SchemaHeaderField("text", "#f1efeb"));
+ alias._pivotField = self._fieldKey;
+ tbox.props.addDocTab(alias, "onRight");
+ }
+ };
+ this._labelSpan.innerHTML = `${self._fieldKey}: `;
+ //MOVED
+ if (node.attrs.docid) {
+ DocServer.GetRefField(node.attrs.docid).
+ then(async dashDoc => dashDoc instanceof Doc && runInAction(() => setDashDoc(dashDoc)));
+ } else {
+ setDashDoc(tbox.props.DataDoc || tbox.dataDoc);
+ }
+
+ //Moved
+ this._reactionDisposer?.();
+ this._reactionDisposer = reaction(() => { // this reaction will update the displayed text whenever the document's fieldKey's value changes
+ const dashVal = this._dashDoc?.[self._fieldKey];
+ return StrCast(dashVal).startsWith(":=") || dashVal === "" ? Doc.Layout(tbox.props.Document)[self._fieldKey] : dashVal;
+ }, fval => {
+ const boolVal = Cast(fval, "boolean", null);
+ if (boolVal === true || boolVal === false) {
+ this._fieldCheck.checked = boolVal;
+ } else {
+ this._fieldSpan.innerHTML = Field.toString(fval as Field) || "";
+ }
+ this._fieldCheck.style.display = (boolVal === true || boolVal === false) ? "inline-block" : "none";
+ this._fieldSpan.style.display = !(fval === true || fval === false) ? (StrCast(fval) ? "" : "inline-block") : "none";
+ }, { fireImmediately: true });
+
+ //MOVED IN ORDER
+ this._fieldWrapper.appendChild(this._labelSpan);
+ this._fieldWrapper.appendChild(this._fieldCheck);
+ this._fieldWrapper.appendChild(this._fieldSpan);
+ this._fieldWrapper.appendChild(this._enumerables);
+ (this as any).dom = this._fieldWrapper;
+ //updateText(false);
+ }
+ //MOVED
+ destroy() {
+ this._reactionDisposer?.();
+ }
+ //moved
+ selectNode() { }
+}
+
+export class FootnoteView {
+ innerView: any;
+ outerView: any;
+ node: any;
+ dom: any;
+ getPos: any;
+
+ constructor(node: any, view: any, getPos: any) {
+ // We'll need these later
+ this.node = node;
+ this.outerView = view;
+ this.getPos = getPos;
+
+ // The node's representation in the editor (empty, for now)
+ this.dom = document.createElement("footnote");
+ this.dom.addEventListener("pointerup", this.toggle, true);
+ // These are used when the footnote is selected
+ this.innerView = null;
+ }
+ selectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = true;
+ this.dom.classList.add("ProseMirror-selectednode");
+ if (!this.innerView) this.open();
+ }
+
+ deselectNode() {
+ const attrs = { ...this.node.attrs };
+ attrs.visibility = false;
+ this.dom.classList.remove("ProseMirror-selectednode");
+ if (this.innerView) this.close();
+ }
+ open() {
+ // Append a tooltip to the outer node
+ const tooltip = this.dom.appendChild(document.createElement("div"));
+ tooltip.className = "footnote-tooltip";
+ // And put a sub-ProseMirror into that
+ this.innerView = new EditorView(tooltip, {
+ // You can use any node as an editor document
+ state: EditorState.create({
+ doc: this.node,
+ plugins: [keymap(baseKeymap),
+ keymap({
+ "Mod-z": () => undo(this.outerView.state, this.outerView.dispatch),
+ "Mod-y": () => redo(this.outerView.state, this.outerView.dispatch),
+ "Mod-b": toggleMark(schema.marks.strong)
+ }),
+ // new Plugin({
+ // view(newView) {
+ // // TODO -- make this work with RichTextMenu
+ // // return FormattedTextBox.getToolTip(newView);
+ // }
+ // })
+ ],
+
+ }),
+ // This is the magic part
+ dispatchTransaction: this.dispatchInner.bind(this),
+ handleDOMEvents: {
+ pointerdown: ((view: any, e: PointerEvent) => {
+ // Kludge to prevent issues due to the fact that the whole
+ // footnote is node-selected (and thus DOM-selected) when
+ // the parent editor is focused.
+ e.stopPropagation();
+ document.addEventListener("pointerup", this.ignore, true);
+ if (this.outerView.hasFocus()) this.innerView.focus();
+ }) as any
+ }
+
+ });
+ setTimeout(() => this.innerView && this.innerView.docView.setSelection(0, 0, this.innerView.root, true), 0);
+ }
+
+ ignore = (e: PointerEvent) => {
+ e.stopPropagation();
+ document.removeEventListener("pointerup", this.ignore, true);
+ }
+
+ toggle = () => {
+ if (this.innerView) this.close();
+ else {
+ this.open();
+ }
+ }
+ close() {
+ this.innerView && this.innerView.destroy();
+ this.innerView = null;
+ this.dom.textContent = "";
+ }
+
+ dispatchInner(tr: any) {
+ const { state, transactions } = this.innerView.state.applyTransaction(tr);
+ this.innerView.updateState(state);
+
+ if (!tr.getMeta("fromOutside")) {
+ const outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1);
+ for (const transaction of transactions) {
+ const steps = transaction.steps;
+ for (const step of steps) {
+ outerTr.step(step.map(offsetMap));
+ }
+ }
+ if (outerTr.docChanged) this.outerView.dispatch(outerTr);
+ }
+ }
+ update(node: any) {
+ if (!node.sameMarkup(this.node)) return false;
+ this.node = node;
+ if (this.innerView) {
+ const state = this.innerView.state;
+ const start = node.content.findDiffStart(state.doc.content);
+ if (start !== null) {
+ let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content);
+ const overlap = start - Math.min(endA, endB);
+ if (overlap > 0) { endA += overlap; endB += overlap; }
+ this.innerView.dispatch(
+ state.tr
+ .replace(start, endB, node.slice(start, endA))
+ .setMeta("fromOutside", true));
+ }
+ }
+ return true;
+ }
+
+ destroy() {
+ if (this.innerView) this.close();
+ }
+
+ stopEvent(event: any) {
+ return this.innerView && this.innerView.dom.contains(event.target);
+ }
+
+ ignoreMutation() { return true; }
+}
+
+export class SummaryView {
+ _collapsed: HTMLElement;
+ _view: any;
+ constructor(node: any, view: any, getPos: any) {
+ this._collapsed = document.createElement("span");
+ this._collapsed.className = this.className(node.attrs.visibility);
+ this._view = view;
+ const js = node.toJSON;
+ node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ this._collapsed.onpointerdown = (e: any) => {
+ const visible = !node.attrs.visibility;
+ const attrs = { ...node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(view.state.doc, getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ view.dispatch(view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : node.attrs.text). // collapse/expand it
+ setNodeMarkup(getPos(), undefined, attrs)); // update the attrs
+ e.preventDefault();
+ e.stopPropagation();
+ this._collapsed.className = this.className(visible);
+ };
+ (this as any).dom = this._collapsed;
+ }
+ selectNode() { }
+
+ deselectNode() { }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ updateSummarizedText(start?: any) {
+ const mtype = this._view.state.schema.marks.summarize;
+ const mtypeInc = this._view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this._view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this._view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (node.isLeaf && !visited.has(node) && !skip) {
+ if (node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this._view.state.doc, start, endPos);
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/SummaryView.tsx b/src/client/views/nodes/formattedText/SummaryView.tsx
new file mode 100644
index 000000000..89908d8ee
--- /dev/null
+++ b/src/client/views/nodes/formattedText/SummaryView.tsx
@@ -0,0 +1,81 @@
+import { TextSelection } from "prosemirror-state";
+import { Fragment, Node, Slice } from "prosemirror-model";
+
+import React = require("react");
+
+interface ISummaryView {
+ node: any;
+ view: any;
+ getPos: any;
+ self: any;
+}
+export class SummaryView extends React.Component<ISummaryView> {
+
+ onPointerDown = (e: any) => {
+ const visible = !this.props.node.attrs.visibility;
+ const attrs = { ...this.props.node.attrs, visibility: visible };
+ let textSelection = TextSelection.create(this.props.view.state.doc, this.props.getPos() + 1);
+ if (!visible) { // update summarized text and save in attrs
+ textSelection = this.updateSummarizedText(this.props.getPos() + 1);
+ attrs.text = textSelection.content();
+ attrs.textslice = attrs.text.toJSON();
+ }
+ this.props.view.dispatch(this.props.view.state.tr.
+ setSelection(textSelection). // select the current summarized text (or where it will be if its collapsed)
+ replaceSelection(!visible ? new Slice(Fragment.fromArray([]), 0, 0) : this.props.node.attrs.text). // collapse/expand it
+ setNodeMarkup(this.props.getPos(), undefined, attrs)); // update the attrs
+ e.preventDefault();
+ e.stopPropagation();
+ const _collapsed = document.getElementById('collapse') as HTMLElement;
+ _collapsed.className = this.className(visible);
+ }
+
+ updateSummarizedText(start?: any) {
+ const mtype = this.props.view.state.schema.marks.summarize;
+ const mtypeInc = this.props.view.state.schema.marks.summarizeInclusive;
+ let endPos = start;
+
+ const visited = new Set();
+ for (let i: number = start + 1; i < this.props.view.state.doc.nodeSize - 1; i++) {
+ let skip = false;
+ this.props.view.state.doc.nodesBetween(start, i, (node: Node, pos: number, parent: Node, index: number) => {
+ if (this.props.node.isLeaf && !visited.has(node) && !skip) {
+ if (this.props.node.marks.find((m: any) => m.type === mtype || m.type === mtypeInc)) {
+ visited.add(node);
+ endPos = i + this.props.node.nodeSize - 1;
+ }
+ else skip = true;
+ }
+ });
+ }
+ return TextSelection.create(this.props.view.state.doc, start, endPos);
+ }
+
+ className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed");
+
+ selectNode() { }
+
+ deselectNode() { }
+
+ render() {
+ const _view = this.props.node.view;
+ const js = this.props.node.toJSon;
+
+ this.props.node.toJSON = function () {
+ return js.apply(this, arguments);
+ };
+
+ const spanCollapsedClassName = this.className(this.props.node.attrs.visibility);
+
+ return (
+ <span
+ className={spanCollapsedClassName}
+ id='collapse'
+ onPointerDown={this.onPointerDown}
+ >
+
+ </span>
+ );
+
+ }
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/TooltipTextMenu.scss b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
new file mode 100644
index 000000000..e2149e9c1
--- /dev/null
+++ b/src/client/views/nodes/formattedText/TooltipTextMenu.scss
@@ -0,0 +1,372 @@
+@import "../views/globalCssVariables";
+.ProseMirror-menu-dropdown-wrap {
+ display: inline-block;
+ position: relative;
+}
+
+.ProseMirror-menu-dropdown {
+ vertical-align: 1px;
+ cursor: pointer;
+ position: relative;
+ padding: 0 15px 0 4px;
+ background: white;
+ border-radius: 2px;
+ text-align: left;
+ font-size: 12px;
+ white-space: nowrap;
+ margin-right: 4px;
+
+ &:after {
+ content: "";
+ border-left: 4px solid transparent;
+ border-right: 4px solid transparent;
+ border-top: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 2px);
+ }
+}
+
+.ProseMirror-menu-submenu-wrap {
+ position: relative;
+ margin-right: -4px;
+}
+
+.ProseMirror-menu-dropdown-menu,
+.ProseMirror-menu-submenu {
+ font-size: 12px;
+ background: white;
+ border: 1px solid rgb(223, 223, 223);
+ min-width: 40px;
+ z-index: 50000;
+ position: absolute;
+ box-sizing: content-box;
+
+ .ProseMirror-menu-dropdown-item {
+ cursor: pointer;
+ z-index: 100000;
+ text-align: left;
+ padding: 3px;
+
+ &:hover {
+ background-color: $light-color-secondary;
+ }
+ }
+}
+
+
+.ProseMirror-menu-submenu-label:after {
+ content: "";
+ border-top: 4px solid transparent;
+ border-bottom: 4px solid transparent;
+ border-left: 4px solid currentColor;
+ opacity: .6;
+ position: absolute;
+ right: 4px;
+ top: calc(50% - 4px);
+}
+
+ .ProseMirror-icon {
+ display: inline-block;
+ // line-height: .8;
+ // vertical-align: -2px; /* Compensate for padding */
+ // padding: 2px 8px;
+ cursor: pointer;
+
+ &.ProseMirror-menu-disabled {
+ cursor: default;
+ }
+
+ svg {
+ fill:white;
+ height: 1em;
+ }
+
+ span {
+ vertical-align: text-top;
+ }
+ }
+
+.wrapper {
+ position: absolute;
+ pointer-events: all;
+ display: flex;
+ align-items: center;
+ transform: translateY(-85px);
+
+ height: 35px;
+ background: #323232;
+ border-radius: 6px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+}
+
+.tooltipMenu, .basic-tools {
+ z-index: 20000;
+ pointer-events: all;
+ padding: 3px;
+ padding-bottom: 5px;
+ display: flex;
+ align-items: center;
+
+ .ProseMirror-example-setup-style hr {
+ padding: 2px 10px;
+ border: none;
+ margin: 1em 0;
+ }
+
+ .ProseMirror-example-setup-style hr:after {
+ content: "";
+ display: block;
+ height: 1px;
+ background-color: silver;
+ line-height: 2px;
+ }
+}
+
+.menuicon {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+
+ #link-drag {
+ background-color: black;
+ }
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: white;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+.menuicon-active {
+ width: 25px;
+ height: 25px;
+ cursor: pointer;
+ text-align: center;
+ line-height: 25px;
+ margin: 0 2px;
+ border-radius: 3px;
+
+ &:hover {
+ background-color: black;
+ }
+
+ &> * {
+ margin-top: 50%;
+ margin-left: 50%;
+ transform: translate(-50%, -50%);
+ }
+
+ svg {
+ fill: greenyellow;
+ width: 18px;
+ height: 18px;
+ }
+}
+
+#colorPicker {
+ position: relative;
+
+ svg {
+ width: 18px;
+ height: 18px;
+ // margin-top: 11px;
+ }
+
+ .buttonColor {
+ position: absolute;
+ top: 24px;
+ left: 1px;
+ width: 24px;
+ height: 4px;
+ margin-top: 0;
+ }
+}
+
+#link-drag {
+ background-color: #323232;
+}
+
+.underline svg {
+ margin-top: 13px;
+}
+
+ .font-size-indicator {
+ font-size: 12px;
+ padding-right: 0px;
+ }
+ .summarize{
+ color: white;
+ height: 20px;
+ text-align: center;
+ }
+
+
+.brush{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 0;
+ stroke: currentColor;
+ fill: currentColor;
+ margin-right: 15px;
+}
+
+.brush-active{
+ display: inline-block;
+ width: 1em;
+ height: 1em;
+ stroke-width: 3;
+ fill: greenyellow;
+ margin-right: 15px;
+}
+
+.dragger-wrapper {
+ color: #eee;
+ height: 22px;
+ padding: 0 5px;
+ box-sizing: content-box;
+ cursor: grab;
+
+ .dragger {
+ width: 18px;
+ height: 100%;
+ display: flex;
+ justify-content: space-evenly;
+ }
+
+ .dragger-line {
+ width: 2px;
+ height: 100%;
+ background-color: black;
+ }
+}
+
+.button-dropdown-wrapper {
+ display: flex;
+ align-content: center;
+
+ &:hover {
+ background-color: black;
+ }
+}
+
+.buttonSettings-dropdown {
+
+ &.ProseMirror-menu-dropdown {
+ width: 10px;
+ height: 25px;
+ margin: 0;
+ padding: 0 2px;
+ background-color: #323232;
+ text-align: center;
+
+ &:after {
+ border-top: 4px solid white;
+ right: 2px;
+ }
+
+ &:hover {
+ background-color: black;
+ }
+ }
+
+ &.ProseMirror-menu-dropdown-menu {
+ min-width: 150px;
+ left: -27px;
+ top: 31px;
+ background-color: #323232;
+ border: 1px solid #4d4d4d;
+ color: $light-color-secondary;
+ // border: none;
+ // border: 1px solid $light-color-secondary;
+ border-radius: 0 6px 6px 6px;
+ padding: 3px;
+ box-shadow: 3px 3px 3px rgba(0, 0, 0, 0.25);
+
+ .ProseMirror-menu-dropdown-item{
+ cursor: default;
+
+ &:last-child {
+ border-bottom: none;
+ }
+
+ &:hover {
+ background-color: #323232;
+ }
+
+ .button-setting, .button-setting-disabled {
+ padding: 2px;
+ border-radius: 2px;
+ }
+
+ .button-setting:hover {
+ cursor: pointer;
+ background-color: black;
+ }
+
+ .separated-button {
+ border-top: 1px solid $light-color-secondary;
+ padding-top: 6px;
+ }
+
+ input {
+ color: black;
+ border: none;
+ border-radius: 1px;
+ padding: 3px;
+ }
+
+ button {
+ padding: 6px;
+ background-color: #323232;
+ border: 1px solid black;
+ border-radius: 1px;
+
+ &:hover {
+ background-color: black;
+ }
+ }
+ }
+
+
+ }
+}
+
+.colorPicker-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: space-between;
+ margin-top: 3px;
+ margin-left: -3px;
+ width: calc(100% + 6px);
+}
+
+button.colorPicker {
+ width: 20px;
+ height: 20px;
+ border-radius: 15px !important;
+ margin: 3px;
+ border: none !important;
+
+ &.active {
+ border: 2px solid white !important;
+ }
+}
diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts
new file mode 100644
index 000000000..46bf481fb
--- /dev/null
+++ b/src/client/views/nodes/formattedText/marks_rts.ts
@@ -0,0 +1,296 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { Doc } from "../../../../new_fields/Doc";
+
+
+const emDOM: DOMOutputSpecArray = ["em", 0];
+const strongDOM: DOMOutputSpecArray = ["strong", 0];
+const codeDOM: DOMOutputSpecArray = ["code", 0];
+
+// :: Object [Specs](#model.MarkSpec) for the marks in the schema.
+export const marks: { [index: string]: MarkSpec } = {
+ // :: MarkSpec A link. Has `href` and `title` attributes. `title`
+ // defaults to the empty string. Rendered and parsed as an `<a>`
+ // element.
+ link: {
+ attrs: {
+ href: {},
+ targetId: { default: "" },
+ linkId: { default: "" },
+ showPreview: { default: true },
+ location: { default: null },
+ title: { default: null },
+ docref: { default: false } // flags whether the linked text comes from a document within Dash. If so, an attribution label is appended after the text
+ },
+ inclusive: false,
+ parseDOM: [{
+ tag: "a[href]", getAttrs(dom: any) {
+ return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title"), targetId: dom.getAttribute("id") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.docref && node.attrs.title ?
+ ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution", title: `${node.attrs.title}` }, node.attrs.title], ["br"]] :
+ ["a", { ...node.attrs, id: node.attrs.linkId + node.attrs.targetId, title: `${node.attrs.title}` }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Coloring on text. Has `color` attribute that defined the color of the marked text.
+ pFontColor: {
+ attrs: {
+ color: { default: "#000" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { color: dom.getAttribute("color") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.color ? ['span', { style: 'color:' + node.attrs.color }] : ['span', 0];
+ }
+ },
+
+ marker: {
+ attrs: {
+ highlight: { default: "transparent" }
+ },
+ inclusive: true,
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ return { highlight: dom.getAttribute("backgroundColor") };
+ }
+ }],
+ toDOM(node: any) {
+ return node.attrs.highlight ? ['span', { style: 'background-color:' + node.attrs.highlight }] : ['span', { style: 'background-color: transparent' }];
+ }
+ },
+
+ // :: MarkSpec An emphasis mark. Rendered as an `<em>` element.
+ // Has parse rules that also match `<i>` and `font-style: italic`.
+ em: {
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
+ toDOM() { return emDOM; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as `<strong>`, parse rules
+ // also match `<b>` and `font-weight: bold`.
+ strong: {
+ parseDOM: [{ tag: "strong" },
+ { tag: "b" },
+ { style: "font-weight" }],
+ toDOM() { return strongDOM; }
+ },
+
+ strikethrough: {
+ parseDOM: [
+ { tag: 'strike' },
+ { style: 'text-decoration=line-through' },
+ { style: 'text-decoration-line=line-through' }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration-line:line-through'
+ }]
+ },
+
+ subscript: {
+ excludes: 'superscript',
+ parseDOM: [
+ { tag: 'sub' },
+ { style: 'vertical-align=sub' }
+ ],
+ toDOM: () => ['sub']
+ },
+
+ superscript: {
+ excludes: 'subscript',
+ parseDOM: [
+ { tag: 'sup' },
+ { style: 'vertical-align=super' }
+ ],
+ toDOM: () => ['sup']
+ },
+
+ mbulletType: {
+ attrs: {
+ bulletType: { default: "decimal" }
+ },
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.bulletType === "decimal" ? "yellow" : node.attrs.bulletType === "upper-alpha" ? "blue" : "green"}`
+ }];
+ }
+ },
+
+ metadata: {
+ toDOM() {
+ return ['span', { style: 'font-size:75%; background:rgba(100, 100, 100, 0.2); ' }];
+ }
+ },
+ metadataKey: {
+ toDOM() {
+ return ['span', { style: 'font-style:italic; ' }];
+ }
+ },
+ metadataVal: {
+ toDOM() {
+ return ['span'];
+ }
+ },
+
+ summarizeInclusive: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: solid") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ inclusive: true,
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: solid; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ summarize: {
+ inclusive: false,
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline") return null;
+ if (p.parentElement.outerHTML.indexOf("text-decoration: underline") !== -1 &&
+ p.parentElement.outerHTML.indexOf("text-decoration-style: dotted") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ },
+ ],
+ toDOM() {
+ return ['span', {
+ style: 'text-decoration: underline; text-decoration-style: dotted; text-decoration-color: rgba(204, 206, 210, 0.92)'
+ }];
+ }
+ },
+
+ underline: {
+ parseDOM: [
+ {
+ tag: "span",
+ getAttrs: (p: any) => {
+ if (typeof (p) !== "string") {
+ const style = getComputedStyle(p);
+ if (style.textDecoration === "underline" || p.parentElement.outerHTML.indexOf("text-decoration-style:line") !== -1) {
+ return null;
+ }
+ }
+ return false;
+ }
+ }
+ // { style: "text-decoration=underline" }
+ ],
+ toDOM: () => ['span', {
+ style: 'text-decoration:underline;text-decoration-style:line'
+ }]
+ },
+
+ search_highlight: {
+ attrs: {
+ selected: { default: false }
+ },
+ parseDOM: [{ style: 'background: yellow' }],
+ toDOM(node: any) {
+ return ['span', {
+ style: `background: ${node.attrs.selected ? "orange" : "yellow"}`
+ }];
+ }
+ },
+
+ // the id of the user who entered the text
+ user_mark: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ },
+ group: "inline",
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ const min = Math.round(node.attrs.modified / 12);
+ const hr = Math.round(min / 60);
+ const day = Math.round(hr / 60 / 24);
+ const remote = node.attrs.userid !== Doc.CurrentUserEmail ? " userMark-remote" : "";
+ return ['span', { class: "userMark-" + uid + remote + " userMark-min-" + min + " userMark-hr-" + hr + " userMark-day-" + day }, 0];
+ }
+ },
+ // the id of the user who entered the text
+ user_tag: {
+ attrs: {
+ userid: { default: "" },
+ modified: { default: "when?" }, // 1 second intervals since 1970
+ tag: { default: "" }
+ },
+ group: "inline",
+ inclusive: false,
+ toDOM(node: any) {
+ const uid = node.attrs.userid.replace(".", "").replace("@", "");
+ return ['span', { class: "userTag-" + uid + " userTag-" + node.attrs.tag }, 0];
+ }
+ },
+
+
+ // :: MarkSpec Code font mark. Represented as a `<code>` element.
+ code: {
+ parseDOM: [{ tag: "code" }],
+ toDOM() { return codeDOM; }
+ },
+
+ /* FONTS */
+ pFontFamily: {
+ attrs: {
+ family: { default: "Crimson Text" },
+ },
+ parseDOM: [{
+ tag: "span", getAttrs(dom: any) {
+ const cstyle = getComputedStyle(dom);
+ if (cstyle.font) {
+ if (cstyle.font.indexOf("Times New Roman") !== -1) return { family: "Times New Roman" };
+ if (cstyle.font.indexOf("Arial") !== -1) return { family: "Arial" };
+ if (cstyle.font.indexOf("Georgia") !== -1) return { family: "Georgia" };
+ if (cstyle.font.indexOf("Comic Sans") !== -1) return { family: "Comic Sans MS" };
+ if (cstyle.font.indexOf("Tahoma") !== -1) return { family: "Tahoma" };
+ if (cstyle.font.indexOf("Crimson") !== -1) return { family: "Crimson Text" };
+ }
+ }
+ }],
+ toDOM: (node) => ['span', {
+ style: `font-family: "${node.attrs.family}";`
+ }]
+ },
+
+ /** FONT SIZES */
+ pFontSize: {
+ attrs: {
+ fontSize: { default: 10 }
+ },
+ parseDOM: [{ style: 'font-size: 10px;' }],
+ toDOM: (node) => ['span', {
+ style: `font-size: ${node.attrs.fontSize}px;`
+ }]
+ },
+};
diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts
new file mode 100644
index 000000000..e7bcf444a
--- /dev/null
+++ b/src/client/views/nodes/formattedText/nodes_rts.ts
@@ -0,0 +1,264 @@
+import React = require("react");
+import { DOMOutputSpecArray, Fragment, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
+import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
+import ParagraphNodeSpec from "./ParagraphNodeSpec";
+
+const blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"],
+ preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0];
+
+// :: Object
+// [Specs](#model.NodeSpec) for the nodes defined in this schema.
+export const nodes: { [index: string]: NodeSpec } = {
+ // :: NodeSpec The top level document node.
+ doc: {
+ content: "block+"
+ },
+
+ footnote: {
+ group: "inline",
+ content: "inline*",
+ inline: true,
+ attrs: {
+ visibility: { default: false }
+ },
+ // This makes the view treat the node as a leaf, even though it
+ // technically has content
+ atom: true,
+ toDOM: () => ["footnote", 0],
+ parseDOM: [{ tag: "footnote" }]
+ },
+
+ paragraph: ParagraphNodeSpec,
+
+ // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`<hr>`).
+ horizontal_rule: {
+ group: "block",
+ parseDOM: [{ tag: "hr" }],
+ toDOM() { return hrDOM; }
+ },
+
+ // :: NodeSpec A heading textblock, with a `level` attribute that
+ // should hold the number 1 to 6. Parsed and serialized as `<h1>` to
+ // `<h6>` elements.
+ heading: {
+ attrs: { level: { default: 1 } },
+ content: "inline*",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "h1", attrs: { level: 1 } },
+ { tag: "h2", attrs: { level: 2 } },
+ { tag: "h3", attrs: { level: 3 } },
+ { tag: "h4", attrs: { level: 4 } },
+ { tag: "h5", attrs: { level: 5 } },
+ { tag: "h6", attrs: { level: 6 } }],
+ toDOM(node: any) { return ["h" + node.attrs.level, 0]; }
+ },
+
+ // :: NodeSpec A code listing. Disallows marks or non-text inline
+ // nodes by default. Represented as a `<pre>` element with a
+ // `<code>` element inside of it.
+ code_block: {
+ content: "text*",
+ marks: "",
+ group: "block",
+ code: true,
+ defining: true,
+ parseDOM: [{ tag: "pre", preserveWhitespace: "full" }],
+ toDOM() { return preDOM; }
+ },
+
+ // :: NodeSpec The text node.
+ text: {
+ group: "inline"
+ },
+
+ dashComment: {
+ attrs: {
+ docid: { default: "" },
+ },
+ inline: true,
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }, "←"];
+ },
+ },
+
+ summary: {
+ inline: true,
+ attrs: {
+ visibility: { default: false },
+ text: { default: undefined },
+ textslice: { default: undefined },
+ },
+ group: "inline",
+ toDOM(node) {
+ const attrs = { style: `width: 40px` };
+ return ["span", { ...node.attrs, ...attrs }];
+ },
+ },
+
+ // :: NodeSpec An inline image (`<img>`) node. Supports `src`,
+ // `alt`, and `href` attributes. The latter two default to the empty
+ // string.
+ image: {
+ inline: true,
+ attrs: {
+ src: {},
+ agnostic: { default: null },
+ width: { default: 100 },
+ alt: { default: null },
+ title: { default: null },
+ float: { default: "left" },
+ location: { default: "onRight" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "img[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ // TODO if we don't define toDom, dragging the image crashes. Why?
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["img", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashDoc: {
+ inline: true,
+ attrs: {
+ width: { default: 200 },
+ height: { default: 100 },
+ title: { default: null },
+ float: { default: "right" },
+ location: { default: "onRight" },
+ hidden: { default: false },
+ fieldKey: { default: "" },
+ docid: { default: "" },
+ alias: { default: "" }
+ },
+ group: "inline",
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ dashField: {
+ inline: true,
+ attrs: {
+ fieldKey: { default: "" },
+ docid: { default: "" }
+ },
+ group: "inline",
+ draggable: false,
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}, height: ${node.attrs.height}` };
+ return ["div", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ video: {
+ inline: true,
+ attrs: {
+ src: {},
+ width: { default: "100px" },
+ alt: { default: null },
+ title: { default: null }
+ },
+ group: "inline",
+ draggable: true,
+ parseDOM: [{
+ tag: "video[src]", getAttrs(dom: any) {
+ return {
+ src: dom.getAttribute("src"),
+ title: dom.getAttribute("title"),
+ alt: dom.getAttribute("alt"),
+ width: Math.min(100, Number(dom.getAttribute("width"))),
+ };
+ }
+ }],
+ toDOM(node) {
+ const attrs = { style: `width: ${node.attrs.width}` };
+ return ["video", { ...node.attrs, ...attrs }];
+ }
+ },
+
+ // :: NodeSpec A hard line break, represented in the DOM as `<br>`.
+ hard_break: {
+ inline: true,
+ group: "inline",
+ selectable: false,
+ parseDOM: [{ tag: "br" }],
+ toDOM() { return brDOM; }
+ },
+
+ ordered_list: {
+ ...orderedList,
+ content: 'list_item+',
+ group: 'block',
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ setFontSize: { default: undefined },
+ setFontFamily: { default: "inherit" },
+ setFontColor: { default: "inherit" },
+ inheritedFontSize: { default: undefined },
+ visibility: { default: true },
+ indent: { default: undefined }
+ },
+ toDOM(node: Node<any>) {
+ if (node.attrs.mapStyle === "bullet") return ['ul', 0];
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ const fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize;
+ const ffam = node.attrs.setFontFamily;
+ const color = node.attrs.setFontColor;
+ return node.attrs.visibility ?
+ ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}; font-family: ${ffam}; color:${color}; margin-left: ${node.attrs.indent}` }, 0] :
+ ['ol', { class: `${map}-ol`, style: `list-style: none;` }];
+ }
+ },
+
+ bullet_list: {
+ ...bulletList,
+ content: 'list_item+',
+ group: 'block',
+ // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }],
+ toDOM(node: Node<any>) {
+ return ['ul', 0];
+ }
+ },
+
+ list_item: {
+ attrs: {
+ bulletStyle: { default: 0 },
+ mapStyle: { default: "decimal" },
+ visibility: { default: true }
+ },
+ ...listItem,
+ content: 'paragraph block*',
+ toDOM(node: any) {
+ const map = node.attrs.bulletStyle ? node.attrs.mapStyle + node.attrs.bulletStyle : "";
+ return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."];
+ //return ["li", { class: `${map}` }, 0];
+ }
+ },
+}; \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/prosemirrorPatches.js b/src/client/views/nodes/formattedText/prosemirrorPatches.js
new file mode 100644
index 000000000..269423482
--- /dev/null
+++ b/src/client/views/nodes/formattedText/prosemirrorPatches.js
@@ -0,0 +1,139 @@
+'use strict';
+
+Object.defineProperty(exports, '__esModule', { value: true });
+
+var prosemirrorInputRules = require('prosemirror-inputrules');
+var prosemirrorTransform = require('prosemirror-transform');
+var prosemirrorModel = require('prosemirror-model');
+
+exports.liftListItem = liftListItem;
+exports.sinkListItem = sinkListItem;
+exports.wrappingInputRule = wrappingInputRule;
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to lift the list item around the selection up into
+// a wrapping list.
+function liftListItem(itemType) {
+ return function (tx, dispatch) {
+ var ref = tx.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ if (!dispatch) { return true }
+ if ($from.node(range.depth - 1).type == itemType) // Inside a parent list
+ { return liftToOuterList(tx, dispatch, itemType, range) }
+ else // Outer list node
+ { return liftOutOfList(tx, dispatch, range) }
+ }
+}
+
+function liftToOuterList(tr, dispatch, itemType, range) {
+ var end = range.end, endOfList = range.$to.end(range.depth);
+ if (end < endOfList) {
+ // There are siblings after the lifted items, which must become
+ // children of the last item
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(end - 1, endOfList, end, endOfList,
+ new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, range.parent.copy())), 1, 0), 1, true));
+ range = new prosemirrorModel.NodeRange(tr.doc.resolve(range.$from.pos), tr.doc.resolve(endOfList), range.depth);
+ }
+ dispatch(tr.lift(range, prosemirrorTransform.liftTarget(range)).scrollIntoView());
+ return true
+}
+
+function liftOutOfList(tr, dispatch, range) {
+ var list = range.parent;
+ // Merge the list items into a single big item
+ for (var pos = range.end, i = range.endIndex - 1, e = range.startIndex; i > e; i--) {
+ pos -= list.child(i).nodeSize;
+ tr.delete(pos - 1, pos + 1);
+ }
+ var $start = tr.doc.resolve(range.start), item = $start.nodeAfter;
+ var atStart = range.startIndex == 0, atEnd = range.endIndex == list.childCount;
+ var parent = $start.node(-1), indexBefore = $start.index(-1);
+ if (!parent.canReplace(indexBefore + (atStart ? 0 : 1), indexBefore + 1,
+ item.content.append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list)))) { return false }
+ var start = $start.pos, end = start + item.nodeSize;
+ // Strip off the surrounding list. At the sides where we're not at
+ // the end of the list, the existing list is closed. At sides where
+ // this is the end, it is overwritten to its end.
+ tr.step(new prosemirrorTransform.ReplaceAroundStep(start - (atStart ? 1 : 0), end + (atEnd ? 1 : 0), start + 1, end - 1,
+ new prosemirrorModel.Slice((atStart ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty)))
+ .append(atEnd ? prosemirrorModel.Fragment.empty : prosemirrorModel.Fragment.from(list.copy(prosemirrorModel.Fragment.empty))),
+ atStart ? 0 : 1, atEnd ? 0 : 1), atStart ? 0 : 1));
+ dispatch(tr.scrollIntoView());
+ return true
+}
+
+// :: (NodeType) → (state: EditorState, dispatch: ?(tr: Transaction)) → bool
+// Create a command to sink the list item around the selection down
+// into an inner list.
+function sinkListItem(itemType) {
+ return function (state, dispatch) {
+ var ref = state.selection;
+ var $from = ref.$from;
+ var $to = ref.$to;
+ var range = $from.blockRange($to, function (node) { return node.childCount && node.firstChild.type == itemType; });
+ if (!range) { return false }
+ var startIndex = range.startIndex;
+ if (startIndex == 0) { return false }
+ var parent = range.parent, nodeBefore = parent.child(startIndex - 1);
+ if (nodeBefore.type != itemType) { return false; }
+
+ if (dispatch) {
+ var nestedBefore = nodeBefore.lastChild && nodeBefore.lastChild.type == parent.type;
+ var inner = prosemirrorModel.Fragment.from(nestedBefore ? itemType.create() : null);
+ let slice = new prosemirrorModel.Slice(prosemirrorModel.Fragment.from(itemType.create(null, prosemirrorModel.Fragment.from(parent.type.create({ ...parent.attrs, fontSize: parent.attrs.fontSize ? parent.attrs.fontSize - 4 : undefined }, inner)))),
+ nestedBefore ? 3 : 1, 0);
+ var before = range.start, after = range.end;
+ dispatch(state.tr.step(new prosemirrorTransform.ReplaceAroundStep(before - (nestedBefore ? 3 : 1), after,
+ before, after, slice, 1, true))
+ .scrollIntoView());
+ }
+ return true
+ }
+}
+
+function findWrappingOutside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var around = parent.contentMatchAt(startIndex).findWrapping(type);
+ if (!around) { return null }
+ var outer = around.length ? around[0] : type;
+ return parent.canReplaceWith(startIndex, endIndex, outer) ? around : null
+}
+
+function findWrappingInside(range, type) {
+ var parent = range.parent;
+ var startIndex = range.startIndex;
+ var endIndex = range.endIndex;
+ var inner = parent.child(startIndex);
+ var inside = type.contentMatch.findWrapping(inner.type);
+ if (!inside) { return null }
+ var lastType = inside.length ? inside[inside.length - 1] : type;
+ var innerMatch = lastType.contentMatch;
+ for (var i = startIndex; innerMatch && i < endIndex; i++) { innerMatch = innerMatch.matchType(parent.child(i).type); }
+ if (!innerMatch || !innerMatch.validEnd) { return null }
+ return inside
+}
+function findWrapping(range, nodeType, attrs, innerRange, customWithAttrs = null) {
+ if (innerRange === void 0) innerRange = range;
+ let withAttrs = (type) => ({ type: type, attrs: null });
+ var around = findWrappingOutside(range, nodeType);
+ var inner = around && findWrappingInside(innerRange, nodeType);
+ if (!inner) { return null }
+ return around.map(withAttrs).concat({ type: nodeType, attrs: attrs }).concat(inner.map(customWithAttrs ? customWithAttrs : withAttrs))
+}
+function wrappingInputRule(regexp, nodeType, getAttrs, joinPredicate, customWithAttrs = null) {
+ return new prosemirrorInputRules.InputRule(regexp, function (state, match, start, end) {
+ var attrs = getAttrs instanceof Function ? getAttrs(match) : getAttrs;
+ var tr = state.tr.delete(start, end);
+ var $start = tr.doc.resolve(start), range = $start.blockRange(), wrapping = range && findWrapping(range, nodeType, attrs, undefined, customWithAttrs);
+ if (!wrapping) { return null }
+ tr.wrap(range, wrapping);
+ var before = tr.doc.resolve(start - 1).nodeBefore;
+ if (before && before.type == nodeType && prosemirrorTransform.canJoin(tr.doc, start - 1) &&
+ (!joinPredicate || joinPredicate(match, before))) { tr.join(start - 1); }
+ return tr
+ })
+} \ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/schema_rts.ts b/src/client/views/nodes/formattedText/schema_rts.ts
new file mode 100644
index 000000000..83561073c
--- /dev/null
+++ b/src/client/views/nodes/formattedText/schema_rts.ts
@@ -0,0 +1,26 @@
+import { Schema, Slice } from "prosemirror-model";
+
+import { nodes } from "./nodes_rts";
+import { marks } from "./marks_rts";
+
+
+// :: Schema
+// This schema rougly corresponds to the document schema used by
+// [CommonMark](http://commonmark.org/), minus the list elements,
+// which are defined in the [`prosemirror-schema-list`](#schema-list)
+// module.
+//
+// To reuse elements from this schema, extend or read from its
+// `spec.nodes` and `spec.marks` [properties](#model.Schema.spec).
+
+export const schema = new Schema({ nodes, marks });
+
+const fromJson = schema.nodeFromJSON;
+
+schema.nodeFromJSON = (json: any) => {
+ const node = fromJson(json);
+ if (json.type === schema.nodes.summary.name) {
+ node.attrs.text = Slice.fromJSON(schema, node.attrs.textslice);
+ }
+ return node;
+}; \ No newline at end of file