` 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 `` element with a
- // `` 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 (`
`) 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 `
`.
- 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) {
- 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) {
- 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/util/prosemirrorPatches.js b/src/client/util/prosemirrorPatches.js
deleted file mode 100644
index 269423482..000000000
--- a/src/client/util/prosemirrorPatches.js
+++ /dev/null
@@ -1,139 +0,0 @@
-'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/util/schema_rts.ts b/src/client/util/schema_rts.ts
deleted file mode 100644
index 83561073c..000000000
--- a/src/client/util/schema_rts.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-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
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/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
deleted file mode 100644
index 3bedb7127..000000000
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ /dev/null
@@ -1,265 +0,0 @@
-@import "../globalCssVariables";
-
-.ProseMirror {
- width: 100%;
- height: 100%;
- min-height: 100%;
-}
-
-.ProseMirror:focus {
- outline: none !important;
-}
-
-.formattedTextBox-cont {
- touch-action: none;
- cursor: text;
- background: inherit;
- padding: 0;
- border-width: 0px;
- border-radius: inherit;
- border-color: $intermediate-color;
- box-sizing: border-box;
- background-color: inherit;
- border-style: solid;
- overflow-y: auto;
- overflow-x: hidden;
- color: initial;
- max-height: 100%;
- display: flex;
- flex-direction: row;
- transition: opacity 1s;
-
- .formattedTextBox-dictation {
- height: 12px;
- width: 10px;
- top: 0px;
- left: 0px;
- position: absolute;
- }
-}
-.formattedTextBox-outer {
- position: relative;
- overflow: auto;
- display: inline-block;
- width: 100%;
- height: 100%;
-}
-
-.formattedTextBox-sidebar-handle {
- position: absolute;
- top: calc(50% - 17.5px);
- width: 10px;
- height: 35px;
- background: lightgray;
- border-radius: 20px;
- cursor:grabbing;
-}
-
-.formattedTextBox-cont>.formattedTextBox-sidebar-handle {
- right: 0;
- left: unset;
-}
-
-.formattedTextBox-sidebar,
-.formattedTextBox-sidebar-inking {
- border-left: dashed 1px black;
- height: 100%;
- display: inline-block;
- position: absolute;
- right: 0;
-
- .collectionfreeformview-container {
- position: relative;
- }
-
- >.formattedTextBox-sidebar-handle {
- right: unset;
- left: -5;
- }
-}
-
-.formattedTextBox-sidebar-inking {
- pointer-events: all;
-}
-
-.formattedTextBox-inner-rounded {
- height: 70%;
- width: 85%;
- position: absolute;
- overflow: auto;
- top: 15%;
- left: 10%;
-}
-
-.formattedTextBox-inner-rounded,
-.formattedTextBox-inner {
- height: 100%;
- white-space: pre-wrap;
-}
-
-// .menuicon {
-// display: inline-block;
-// border-right: 1px solid rgba(0, 0, 0, 0.2);
-// color: #888;
-// line-height: 1;
-// padding: 0 7px;
-// margin: 1px;
-// cursor: pointer;
-// text-align: center;
-// min-width: 1.4em;
-// }
-
-.strong,
-.heading {
- font-weight: bold;
-}
-
-.em {
- font-style: italic;
-}
-
-.userMarkOpen {
- background: rgba(255, 255, 0, 0.267);
- display: inline;
-}
-
-.userMark {
- background: rgba(255, 255, 0, 0.267);
- font-size: 2px;
- display: inline-grid;
- overflow: hidden;
- text-overflow: ellipsis;
- white-space: nowrap;
- width: 10px;
- min-height: 10px;
- text-align: center;
- align-content: center;
-}
-
-footnote {
- display: inline-block;
- position: relative;
- cursor: pointer;
-
- div {
- padding: 0 !important;
- }
-}
-
-footnote::after {
- content: counter(prosemirror-footnote);
- vertical-align: super;
- font-size: 75%;
- counter-increment: prosemirror-footnote;
-}
-
-.ProseMirror {
- counter-reset: prosemirror-footnote;
-}
-
-.footnote-tooltip {
- cursor: auto;
- font-size: 75%;
- position: absolute;
- left: -30px;
- top: calc(100% + 10px);
- background: silver;
- padding: 3px;
- border-radius: 2px;
- max-width: 100px;
- min-width: 50px;
- width: max-content;
-}
-
-.prosemirror-attribution {
- font-size: 8px;
-}
-
-.footnote-tooltip::before {
- border: 5px solid silver;
- border-top-width: 0px;
- border-left-color: transparent;
- border-right-color: transparent;
- position: absolute;
- top: -5px;
- left: 27px;
- content: " ";
- height: 0;
- width: 0;
-}
-
-
-.formattedTextBox-inlineComment {
- position: relative;
- width: 40px;
- height: 20px;
- &::before {
- content: "→";
- }
- &:hover {
- background: orange;
- }
-}
-
-.formattedTextBox-summarizer {
- opacity: 0.5;
- position: relative;
- width: 40px;
- height: 20px;
- &::after {
- content: "←";
- }
-}
-
-.formattedTextBox-summarizer-collapsed {
- opacity: 0.5;
- position: relative;
- width: 40px;
- height: 20px;
- &::after {
- content: "...";
- }
-}
-
-.ProseMirror {
- touch-action: none;
- span {
- font-family: inherit;
- }
-
- ol, ul {
- counter-reset: deci1 0 multi1 0;
- padding-left: 1em;
- font-family: inherit;
- }
- ol {
- margin-left: 1em;
- font-family: inherit;
- }
-
- .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; }
- .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;}
- .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;}
- .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; }
- .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; }
- .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; }
-
- .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em }
- .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
- .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
- .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
-
- .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
- .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
- .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
- .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
- .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
- .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
- .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
-
- .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
- .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
- .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
- .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
-}
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
deleted file mode 100644
index e65453aa0..000000000
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ /dev/null
@@ -1,1306 +0,0 @@
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { isEqual } from "lodash";
-import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
-import { observer } from "mobx-react";
-import { baseKeymap } from "prosemirror-commands";
-import { history } from "prosemirror-history";
-import { inputRules } from 'prosemirror-inputrules';
-import { keymap } from "prosemirror-keymap";
-import { Fragment, 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, SummaryView } from "../../util/RichTextSchema";
-// import { DashDocCommentView, DashDocView, DashFieldView, FootnoteView, SummaryView } from "../../util/RichTextSchema";
-import { OrderedListView } from "../../util/RichTextSchema";
-import { ImageResizeView } from "../../util/ImageResizeView";
-
-import { DashDocCommentView } from "../../util/DashDocCommentView";
-import { DashFieldView } from "../../util/DashFieldView";
-import { FootnoteView } from "../../util/FootnoteView";
-import { SummaryView } from "../../util/SummaryView";
-import { DashDocView } from "../../util/DashDocView";
-
-import { schema } from "../../util/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");
-
-library.add(faEdit);
-library.add(faSmile, faTextHeight, faUpload);
-
-export interface FormattedTextBoxProps {
- hideOnLeave?: boolean;
- makeLink?: () => Opt;
- xMargin?: number;
- yMargin?: number;
-}
-
-const richTextSchema = createSchema({
- documentText: "string"
-});
-
-export const GoogleRef = "googleDocId";
-
-type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
-const RichTextDocument = makeInterface(richTextSchema, documentSchema);
-
-type PullHandler = (exportState: Opt, dataDoc: Doc) => void;
-
-@observer
-export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
- public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
- public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
- public static Instance: FormattedTextBox;
- public ProseRef?: HTMLDivElement;
- private _ref: React.RefObject = React.createRef();
- private _scrollRef: React.RefObject = React.createRef();
- private _editorView: Opt;
- private _applyingChange: boolean = false;
- private _searchIndex = 0;
- private _sidebarMovement = 0;
- private _lastX = 0;
- private _lastY = 0;
- private _undoTyping?: UndoManager.Batch;
- private _disposers: { [name: string]: IReactionDisposer } = {};
- private dropDisposer?: DragManager.DragDropDisposer;
-
- @computed get _recording() { return this.dataDoc.audioState === "recording"; }
- set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
-
- @observable private _entered = false;
-
- public static FocusedBox: FormattedTextBox | undefined;
- public static SelectOnLoad = "";
- public static SelectOnLoadChar = "";
- public static IsFragment(html: string) {
- return html.indexOf("data-pm-slice") !== -1;
- }
- public static GetHref(html: string): string {
- const parser = new DOMParser();
- const parsedHtml = parser.parseFromString(html, 'text/html');
- if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
- (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
- return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
- }
- return "";
- }
- public static GetDocFromUrl(url: string) {
- if (url.startsWith(document.location.origin)) {
- const split = new URL(url).pathname.split("doc/");
- const docid = split[split.length - 1];
- return docid;
- }
- return "";
- }
-
- @undoBatch
- public setFontColor(color: string) {
- const view = this._editorView!;
- if (view.state.selection.from === view.state.selection.to) return false;
- if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
- this.layoutDoc.color = color;
- }
- const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
- view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
- return true;
- }
-
- constructor(props: any) {
- super(props);
- FormattedTextBox.Instance = this;
- this.updateHighlights();
- }
-
- public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
-
- linkOnDeselect: Map = new Map();
-
- doLinkOnDeselect() {
- Array.from(this.linkOnDeselect.entries()).map(entry => {
- const key = entry[0];
- const value = entry[1];
- const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
- DocServer.GetRefField(value).then(doc => {
- DocServer.GetRefField(id).then(linkDoc => {
- this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
- DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
- if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
- else DocUtils.MakeLink({ doc: this.props.Document }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
- });
- });
- });
- this.linkOnDeselect.clear();
- }
-
- dispatchTransaction = (tx: Transaction) => {
- if (this._editorView) {
- const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
- if (metadata) {
- const range = tx.selection.$from.blockRange(tx.selection.$to);
- let text = range ? tx.doc.textBetween(range.start, range.end) : "";
- let textEndSelection = tx.selection.to;
- for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { }
- text = text.substr(0, textEndSelection - range!.start);
- text = text.split(" ")[text.split(" ").length - 1];
- const split = text.split("::");
- if (split.length > 1 && split[1]) {
- const key = split[0];
- const value = split[split.length - 1];
- this.linkOnDeselect.set(key, value);
-
- const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
- const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value });
- const mval = this._editorView.state.schema.marks.metadataVal.create();
- const offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
- tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
- this.dataDoc[key] = value;
- }
- }
- const state = this._editorView.state.apply(tx);
- this._editorView.updateState(state);
- (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
-
- const tsel = this._editorView.state.selection.$from;
- tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
- const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
- const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
- if (!this._applyingChange) {
- this._applyingChange = true;
- this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
- if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
- this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), curText);
- this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
- } else { // if we've deleted all the text in a note driven by a template, then restore the template data
- this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data)));
- this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
- }
- this._applyingChange = false;
- }
- this.updateTitle();
- this.tryUpdateHeight();
- }
- }
-
- updateTitle = () => {
- if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
- StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) {
- const str = this._editorView.state.doc.textContent;
- const titlestr = str.substr(0, Math.min(40, str.length));
- this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
- }
- }
-
- // needs a better API for taking in a set of words with target documents instead of just one target
- public hyperlinkTerms = (terms: string[], target: Doc) => {
- if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
- const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- const tr = this._editorView.state.tr;
- const flattened: TextSelection[] = [];
- res.map(r => r.map(h => flattened.push(h)));
- const lastSel = Math.min(flattened.length - 1, this._searchIndex);
- this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
- const alink = DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, "automatic")!;
- const link = this._editorView.state.schema.marks.link.create({
- href: Utils.prepend("/doc/" + alink[Id]),
- title: "a link", location: location, linkId: alink[Id], targetId: target[Id]
- });
- this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link));
- }
- }
- public highlightSearchTerms = (terms: string[]) => {
- if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
- const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
- const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
- let tr = this._editorView.state.tr;
- const flattened: TextSelection[] = [];
- res.map(r => r.map(h => flattened.push(h)));
- const lastSel = Math.min(flattened.length - 1, this._searchIndex);
- flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
- this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
- this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
- }
- }
-
- public unhighlightSearchTerms = () => {
- if (this._editorView && (this._editorView as any).docView) {
- const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
- const end = this._editorView.state.doc.nodeSize - 2;
- this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
- }
- }
- adoptAnnotation = (start: number, end: number, mark: Mark) => {
- const view = this._editorView!;
- const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail });
- view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
- }
- protected createDropTarget = (ele: HTMLDivElement) => {
- this.ProseRef = ele;
- this.dropDisposer?.();
- ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
- }
-
- @undoBatch
- @action
- drop = async (e: Event, de: DragManager.DropEvent) => {
- if (de.complete.docDragData) {
- const draggedDoc = de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0];
- // replace text contents whend dragging with Alt
- if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) {
- if (draggedDoc.data instanceof RichTextField) {
- Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text);
- e.stopPropagation();
- }
- // embed document when dragging with a userDropAction or an embedDoc flag set
- } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) {
- const target = de.complete.docDragData.droppedDocuments[0];
- // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
- // if (link) {
- target._fitToBox = true;
- const node = schema.nodes.dashDoc.create({
- width: target[WidthSym](), height: target[HeightSym](),
- title: "dashDoc", docid: target[Id],
- float: "right"
- });
- const view = this._editorView!;
- view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
- this.tryUpdateHeight();
- e.stopPropagation();
- // }
- } // otherwise, fall through to outer collection to handle drop
- } else if (de.complete.linkDragData) {
- de.complete.linkDragData.linkDropCallback = this.linkDrop;
- }
- }
- linkDrop = (data: DragManager.LinkDragData) => {
- const linkDoc = data.linkDocument!;
- const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-";
- const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : "";
- this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id);
- }
-
- getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
- let offset = 0;
-
- if (context === node) return { from: offset, to: offset + node.nodeSize };
-
- if (node.isBlock) {
- // tslint:disable-next-line: prefer-for-of
- for (let i = 0; i < (context.content as any).content.length; i++) {
- const result = this.getNodeEndpoints((context.content as any).content[i], node);
- if (result) {
- return {
- from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
- to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
- };
- }
- offset += (context.content as any).content[i].nodeSize;
- }
- return null;
- } else {
- return null;
- }
- }
-
-
- //Recursively finds matches within a given node
- findInNode(pm: EditorView, node: Node, find: string) {
- let ret: TextSelection[] = [];
-
- if (node.isTextblock) {
- let index = 0, foundAt;
- const ep = this.getNodeEndpoints(pm.state.doc, node);
- while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
- const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
- ret.push(sel);
- index = index + foundAt + find.length;
- }
- } else {
- node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
- }
- return ret;
- }
- static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"];
-
- updateHighlights = () => {
- clearStyleSheetRules(FormattedTextBox._userStyleSheet);
- if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
- }
- if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
- }
- if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
- }
- if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
- }
- if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
- }
- if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" });
- }
- if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
- const min = Math.round(Date.now() / 1000 / 60);
- numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
- setTimeout(() => this.updateHighlights());
- }
- if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
- addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
- const hr = Math.round(Date.now() / 1000 / 60 / 60);
- numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
- }
- }
-
- sidebarDown = (e: React.PointerEvent) => {
- this._lastX = e.clientX;
- this._lastY = e.clientY;
- this._sidebarMovement = 0;
- document.addEventListener("pointermove", this.sidebarMove);
- document.addEventListener("pointerup", this.sidebarUp);
- e.stopPropagation();
- e.preventDefault(); // prevents text from being selected during drag
- }
- sidebarMove = (e: PointerEvent) => {
- const bounds = this.CurrentDiv.getBoundingClientRect();
- this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY));
- this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%";
- }
- sidebarUp = (e: PointerEvent) => {
- document.removeEventListener("pointermove", this.sidebarMove);
- document.removeEventListener("pointerup", this.sidebarUp);
- }
-
- toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%");
-
- public static get DefaultLayout(): Doc | string | undefined {
- return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null);
- }
- specificContextMenu = (e: React.MouseEvent): void => {
- const cm = ContextMenu.Instance;
-
- const funcs: ContextMenuProps[] = [];
- this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
- funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
- !this.props.Document.rootDocument && funcs.push({
- description: "Make Template", event: () => {
- this.props.Document.isTemplateDoc = makeTemplate(this.props.Document);
- Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
- }, icon: "eye"
- });
- funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
- funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
- funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
- funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
-
- const highlighting: ContextMenuProps[] = [];
- ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
- highlighting.push({
- description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
- e.stopPropagation();
- if (FormattedTextBox._highlights.indexOf(option) === -1) {
- FormattedTextBox._highlights.push(option);
- } else {
- FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
- }
- this.updateHighlights();
- }, icon: "expand-arrows-alt"
- }));
- funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" });
-
- ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
-
- const change = cm.findByDescription("Change Perspective...");
- const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : [];
-
- const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null);
- DocListCast(noteTypesDoc?.data).forEach(note => {
- changeItems.push({
- description: StrCast(note.title), event: undoBatch(() => {
- Doc.setNativeView(this.props.Document);
- Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
- }), icon: "eye"
- });
- });
- changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
- !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
-
- const open = cm.findByDescription("Add a Perspective...");
- const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
-
- openItems.push({
- description: "FreeForm", event: undoBatch(() => {
- const alias = Doc.MakeAlias(this.rootDoc);
- Doc.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform");
- this.props.addDocTab(alias, "onRight");
- }), icon: "eye"
- });
- !open && cm.addItem({ description: "Add a Perspective...", subitems: openItems, icon: "external-link-alt" });
-
- }
-
- recordDictation = () => {
- DictationManager.Controls.listen({
- interimHandler: this.setCurrentBulletContent,
- continuous: { indefinite: false },
- }).then(results => {
- if (results && [DictationManager.Controls.Infringed].includes(results)) {
- DictationManager.Controls.stop();
- }
- //this._editorView!.focus();
- });
- }
- stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); };
-
- @action
- toggleMenubar = () => {
- this.props.Document._chromeStatus = this.props.Document._chromeStatus === "disabled" ? "enabled" : "disabled";
- }
-
- recordBullet = async () => {
- const completedCue = "end session";
- const results = await DictationManager.Controls.listen({
- interimHandler: this.setCurrentBulletContent,
- continuous: { indefinite: false },
- terminators: [completedCue, "bullet", "next"]
- });
- if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) {
- DictationManager.Controls.stop();
- return;
- }
- this.nextBullet(this._editorView!.state.selection.to);
- setTimeout(this.recordBullet, 2000);
- }
-
- setCurrentBulletContent = (value: string) => {
- if (this._editorView) {
- const state = this._editorView.state;
- const now = Date.now();
- let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
- if (!this._break && state.selection.to !== state.selection.from) {
- for (let i = state.selection.from; i <= state.selection.to; i++) {
- const pos = state.doc.resolve(i);
- const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
- if (um) {
- mark = um;
- break;
- }
- }
- }
- const recordingStart = DateCast(this.props.Document.recordingStart).date.getTime();
- this._break = false;
- value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value;
- const from = state.selection.from;
- const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
- this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1)));
- }
- }
-
- nextBullet = (pos: number) => {
- if (this._editorView) {
- const frag = Fragment.fromArray(this.newListItems(2));
- if (this._editorView.state.doc.resolve(pos).depth >= 2) {
- const slice = new Slice(frag, 2, 2);
- let state = this._editorView.state;
- this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
- pos += 4;
- state = this._editorView.state;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
- }
- }
- }
-
- private newListItems = (count: number) => {
- return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
- }
-
- _keymap: any = undefined;
- _rules: RichTextRules | undefined;
- @computed get config() {
- this._keymap = buildKeymap(schema, this.props);
- this._rules = new RichTextRules(this.props.Document, this);
- return {
- schema,
- plugins: [
- inputRules(this._rules.inpRules),
- this.richTextMenuPlugin(),
- history(),
- keymap(this._keymap),
- keymap(baseKeymap),
- new Plugin({
- props: {
- attributes: { class: "ProseMirror-example-setup-style" }
- }
- }),
- formattedTextBoxCommentPlugin
- ]
- };
- }
-
- makeLinkToSelection(linkDocId: string, title: string, location: string, targetDocId: string) {
- if (this._editorView) {
- const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
- this._editorView.dispatch(this._editorView.state.tr.removeMark(this._editorView.state.selection.from, this._editorView.state.selection.to, this._editorView.state.schema.marks.link).
- addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link));
- }
- }
- componentDidMount() {
- this._disposers.buttonBar = reaction(
- () => DocumentButtonBar.Instance,
- instance => {
- if (instance) {
- this.pullFromGoogleDoc(this.checkState);
- this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => instance.isAnimatingFetch = true);
- }
- }
- );
- this._disposers.linkMaker = reaction(
- () => this.props.makeLink?.(),
- (linkDoc: Opt) => {
- if (linkDoc) {
- const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
- const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
- this.makeLinkToSelection(linkDoc[Id], anchor2Title, "onRight", anchor2Id);
- }
- },
- { fireImmediately: true }
- );
- this._disposers.editorState = reaction(
- () => {
- if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) {
- return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
- }
- return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
- },
- incomingValue => {
- if (incomingValue !== undefined && this._editorView && !this._applyingChange) {
- const updatedState = JSON.parse(incomingValue);
- this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
- this.tryUpdateHeight();
- }
- }
- );
- this._disposers.pullDoc = reaction(
- () => this.props.Document[Pulls],
- () => {
- if (!DocumentButtonBar.hasPulledHack) {
- DocumentButtonBar.hasPulledHack = true;
- const unchanged = this.dataDoc.unchanged;
- this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
- }
- }
- );
- this._disposers.pushDoc = reaction(
- () => this.props.Document[Pushes],
- () => {
- if (!DocumentButtonBar.hasPushedHack) {
- DocumentButtonBar.hasPushedHack = true;
- this.pushToGoogleDoc();
- }
- }
- );
- this._disposers.height = reaction(
- () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight],
- () => this.tryUpdateHeight()
- );
-
- this.setupEditor(this.config, this.props.fieldKey);
-
- this._disposers.search = reaction(() => this.rootDoc.searchMatch,
- search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
- { fireImmediately: true });
-
- this._disposers.record = reaction(() => this._recording,
- () => {
- if (this._recording) {
- setTimeout(action(() => {
- this.stopDictation(true);
- setTimeout(() => this.recordDictation(), 500);
- }), 500);
- } else setTimeout(() => this.stopDictation(true), 0);
- }
- );
- this._disposers.scrollToRegion = reaction(
- () => StrCast(this.layoutDoc.scrollToLinkID),
- async (scrollToLinkID) => {
- const findLinkFrag = (frag: Fragment, editor: EditorView) => {
- const nodes: Node[] = [];
- frag.forEach((node, index) => {
- const examinedNode = findLinkNode(node, editor);
- if (examinedNode && examinedNode.textContent) {
- nodes.push(examinedNode);
- start += index;
- }
- });
- return { frag: Fragment.fromArray(nodes), start: start };
- };
- const findLinkNode = (node: Node, editor: EditorView) => {
- if (!node.isText) {
- const content = findLinkFrag(node.content, editor);
- return node.copy(content.frag);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
- return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
- };
-
- let start = -1;
- if (this._editorView && scrollToLinkID) {
- const editor = this._editorView;
- const ret = findLinkFrag(editor.state.doc.content, editor);
-
- if (ret.frag.size > 2 && ret.start >= 0) {
- let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
- if (ret.frag.firstChild) {
- selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
- }
- editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
- const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
- setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
- setTimeout(() => this.unhighlightSearchTerms(), 2000);
- }
- Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
- }
-
- },
- { fireImmediately: true }
- );
- this._disposers.scroll = reaction(() => NumCast(this.props.Document.scrollPos),
- pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true }
- );
-
- setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0)));
- }
-
- pushToGoogleDoc = async () => {
- this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => {
- const modes = GoogleApiClientUtils.Docs.WriteMode;
- let mode = modes.Replace;
- let reference: Opt = Cast(this.dataDoc[GoogleRef], "string");
- if (!reference) {
- mode = modes.Insert;
- reference = { title: StrCast(this.dataDoc.title) };
- }
- const redo = async () => {
- if (this._editorView && reference) {
- const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
- const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
- response && (this.dataDoc[GoogleRef] = response.documentId);
- const pushSuccess = response !== undefined && !("errors" in response);
- dataDoc.unchanged = pushSuccess;
- DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
- }
- };
- const undo = () => {
- if (!exportState) {
- return;
- }
- const content: GoogleApiClientUtils.Docs.Content = {
- text: exportState.text,
- requests: []
- };
- if (reference && content) {
- GoogleApiClientUtils.Docs.write({ reference, content, mode });
- }
- };
- UndoManager.AddEvent({ undo, redo });
- redo();
- });
- }
-
- pullFromGoogleDoc = async (handler: PullHandler) => {
- const dataDoc = this.dataDoc;
- const documentId = StrCast(dataDoc[GoogleRef]);
- let exportState: Opt;
- if (documentId) {
- exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
- }
- UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
- }
-
- updateState = (exportState: Opt, dataDoc: Doc) => {
- let pullSuccess = false;
- if (exportState !== undefined) {
- pullSuccess = true;
- dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
- setTimeout(() => {
- if (this._editorView) {
- const state = this._editorView.state;
- const end = state.doc.content.size - 1;
- this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
- }
- }, 0);
- dataDoc.title = exportState.title;
- this.rootDoc.customTitle = true;
- dataDoc.unchanged = true;
- } else {
- delete dataDoc[GoogleRef];
- }
- DocumentButtonBar.Instance.startPullOutcome(pullSuccess);
- }
-
- checkState = (exportState: Opt, dataDoc: Doc) => {
- if (exportState && this._editorView) {
- const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc);
- const equalTitles = dataDoc.title === exportState.title;
- const unchanged = equalContent && equalTitles;
- dataDoc.unchanged = unchanged;
- DocumentButtonBar.Instance.setPullState(unchanged);
- }
- }
-
- clipboardTextSerializer = (slice: Slice): string => {
- let text = "", separated = true;
- const from = 0, to = slice.content.size;
- slice.content.nodesBetween(from, to, (node, pos) => {
- if (node.isText) {
- text += node.text!.slice(Math.max(from, pos) - pos, to - pos);
- separated = false;
- } else if (!separated && node.isBlock) {
- text += "\n";
- separated = true;
- } else if (node.type.name === "hard_break") {
- text += "\n";
- }
- }, 0);
- return text;
- }
-
- sliceSingleNode(slice: Slice) {
- return slice.openStart === 0 && slice.openEnd === 0 && slice.content.childCount === 1 ? slice.content.firstChild : null;
- }
-
- handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
- const cbe = event as ClipboardEvent;
- const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
- const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
- if (pdfDocId && pdfRegionId) {
- DocServer.GetRefField(pdfDocId).then(pdfDoc => {
- DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
- if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
- setTimeout(async () => {
- const targetField = Doc.LayoutFieldKey(pdfDoc);
- const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations
- targetAnnotations?.push(pdfRegion);
- });
-
- const link = DocUtils.MakeLink({ doc: this.props.Document }, { doc: pdfRegion }, "PDF pasted");
- if (link) {
- cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
- const linkId = link[Id];
- const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
- slice = new Slice(frag, slice.openStart, slice.openEnd);
- const tr = view.state.tr.replaceSelection(slice);
- view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
- }
- }
- });
- });
- return true;
- }
- return false;
-
-
- function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
- const nodes: Node[] = [];
- frag.forEach(node => nodes.push(marker(node)));
- return Fragment.fromArray(nodes);
- }
- function addLinkMark(node: Node, title: string, linkId: string) {
- if (!node.isText) {
- const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
- return node.copy(content);
- }
- const marks = [...node.marks];
- const linkIndex = marks.findIndex(mark => mark.type.name === "link");
- const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
- marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
- return node.mark(marks);
- }
- }
-
- private setupEditor(config: any, fieldKey: string) {
- const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null);
- const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"];
- const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField);
- if (this.ProseRef) {
- const self = this;
- this._editorView?.destroy();
- this._editorView = new EditorView(this.ProseRef, {
- state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
- handleScrollToSelection: (editorView) => {
- const docPos = editorView.coordsAtPos(editorView.state.selection.from);
- const viewRect = self._ref.current!.getBoundingClientRect();
- if (docPos.top < viewRect.top || docPos.top > viewRect.bottom) {
- docPos && (self._scrollRef.current!.scrollTop += (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale);
- }
- return true;
- },
- dispatchTransaction: this.dispatchTransaction,
- nodeViews: {
- //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); },
- // 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, outerView, getPos }); }
- },
- clipboardTextSerializer: this.clipboardTextSerializer,
- handlePaste: this.handlePaste,
- });
- const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
- if (startupText) {
- const { state: { tr }, dispatch } = this._editorView;
- dispatch(tr.insertText(startupText));
- }
- }
-
- const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad;
- if (selectOnLoad && !this.props.dontRegisterView) {
- FormattedTextBox.SelectOnLoad = "";
- this.props.select(false);
- FormattedTextBox.SelectOnLoadChar && this._editorView!.dispatch(this._editorView!.state.tr.insertText(FormattedTextBox.SelectOnLoadChar));
- FormattedTextBox.SelectOnLoadChar = "";
-
- }
- (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
- // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
- this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
- }
- getFont(font: string) {
- switch (font) {
- case "Arial": return schema.marks.arial.create();
- case "Times New Roman": return schema.marks.timesNewRoman.create();
- case "Georgia": return schema.marks.georgia.create();
- case "Comic Sans MS": return schema.marks.comicSans.create();
- case "Tahoma": return schema.marks.tahoma.create();
- case "Impact": return schema.marks.impact.create();
- case "ACrimson Textrial": return schema.marks.crimson.create();
- }
- return schema.marks.arial.create();
- }
-
- componentWillUnmount() {
- Object.values(this._disposers).forEach(disposer => disposer?.());
- this._editorView?.destroy();
- }
-
- static _downEvent: any;
- _downX = 0;
- _downY = 0;
- _break = false;
- onPointerDown = (e: React.PointerEvent): void => {
- if (this._recording && !e.ctrlKey && e.button === 0) {
- this.stopDictation(true);
- this._break = true;
- const state = this._editorView!.state;
- const to = state.selection.to;
- const updated = TextSelection.create(state.doc, to, to);
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to));
- e.preventDefault();
- e.stopPropagation();
- if (this._recording) setTimeout(() => this.recordDictation(), 500);
- }
- this._downX = e.clientX;
- this._downY = e.clientY;
- this.doLinkOnDeselect();
- FormattedTextBox._downEvent = true;
- FormattedTextBoxComment.textBox = this;
- if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) {
- e.preventDefault();
- }
- if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
- if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar
- e.stopPropagation();
- }
- }
- if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
- e.preventDefault();
- }
- }
-
- onPointerUp = (e: React.PointerEvent): void => {
- if (!FormattedTextBox._downEvent) return;
- FormattedTextBox._downEvent = false;
- if (!(e.nativeEvent as any).formattedHandled) {
- FormattedTextBoxComment.textBox = this;
- FormattedTextBoxComment.update(this._editorView!);
- }
- (e.nativeEvent as any).formattedHandled = true;
-
- if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
- e.stopPropagation();
- }
- this._downX = this._downY = Number.NaN;
- }
-
- @action
- onFocused = (e: React.FocusEvent): void => {
- FormattedTextBox.FocusedBox = this;
- this.tryUpdateHeight();
-
- // see if we need to preserve the insertion point
- const prosediv = this.ProseRef?.children?.[0] as any;
- const keeplocation = prosediv?.keeplocation;
- prosediv && (prosediv.keeplocation = undefined);
- const pos = this._editorView?.state.selection.$from.pos || 1;
- keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
- const coords = !Number.isNaN(this._downX) ? { left: this._downX, top: this._downY, bottom: this._downY, right: this._downX } : this._editorView?.coordsAtPos(pos);
-
- // jump rich text menu to this textbox
- const bounds = this._ref.current?.getBoundingClientRect();
- if (bounds && this.props.Document._chromeStatus !== "disabled") {
- const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width);
- let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height);
- if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) {
- y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height);
- }
- RichTextMenu.Instance.jumpTo(x, y);
- }
- }
- onPointerWheel = (e: React.WheelEvent): void => {
- // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
- if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
- e.stopPropagation();
- }
- }
-
- static _bulletStyleSheet: any = addStyleSheet();
- static _userStyleSheet: any = addStyleSheet();
-
- onClick = (e: React.MouseEvent): void => {
- if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
- const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
- const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
- if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
- e.preventDefault();
- }
- if (!node && this.ProseRef) {
- const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div
- if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
- }
- }
- }
- if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
- (e.nativeEvent as any).formattedHandled = true;
- // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) {
- // let href = (e.target as any).href;
- // let location: string;
- // if ((e.target as any).attributes.location) {
- // location = (e.target as any).attributes.location.value;
- // }
- // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
- // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
- // if (node) {
- // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
- // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
- // href = link && link.attrs.href;
- // location = link && link.attrs.location;
- // }
- // }
- // if (href) {
- // if (href.indexOf(Utils.prepend("/doc/")) === 0) {
- // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- // if (linkClicked) {
- // DocServer.GetRefField(linkClicked).then(async linkDoc => {
- // (linkDoc instanceof Doc) &&
- // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, location ? location : "inTab"), false);
- // });
- // }
- // } else {
- // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
- // this.props.addDocument && this.props.addDocument(webDoc);
- // }
- // e.stopPropagation();
- // e.preventDefault();
- // }
- // }
-
- if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) {
- this.props.select(e.ctrlKey);
- this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
- }
- }
-
- // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
- hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) {
- clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
- const pos = this._editorView!.posAtCoords({ left: x, top: y });
- if (pos && this.props.isSelected(true)) {
- // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined;
- //const node = this._editorView!.state.doc.nodeAt(pos.pos);
- const $pos = this._editorView!.state.doc.resolve(pos.pos);
- let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined;
- if ($pos.node().type === schema.nodes.ordered_list) {
- for (let off = 1; off < 100; off++) {
- const pos = this._editorView!.posAtCoords({ left: x + off, top: y });
- const node = pos && this._editorView!.state.doc.nodeAt(pos.pos);
- if (node?.type === schema.nodes.list_item) {
- list_node = node;
- break;
- }
- }
- }
- if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) {
- if (select) {
- const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1);
- if (!highlightOnly) {
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos)));
- }
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
- } else if (Math.abs(pos.pos - pos.inside) < 2) {
- if (!highlightOnly) {
- const offset = this._editorView!.state.doc.nodeAt(pos.inside)?.type === schema.nodes.ordered_list ? 1 : 0;
- this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside + offset, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility }));
- this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside + offset)));
- }
- addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
- }
- }
- }
- }
- onMouseUp = (e: React.MouseEvent): void => {
- e.stopPropagation();
-
- const view = this._editorView as any;
- // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
- // are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
- if (view.mouseDown) {
- const originalUpHandler = view.mouseDown.up;
- view.root.removeEventListener("mouseup", originalUpHandler);
- view.mouseDown.up = (e: MouseEvent) => {
- !(e as any).formattedHandled && originalUpHandler(e);
- // e.stopPropagation();
- (e as any).formattedHandled = true;
- };
- view.root.addEventListener("mouseup", view.mouseDown.up);
- }
- }
-
- richTextMenuPlugin() {
- return new Plugin({
- view(newView) {
- RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView);
- return RichTextMenu.Instance;
- }
- });
- }
-
- public static HadSelection: boolean = false;
- onBlur = (e: any) => {
- FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
- //DictationManager.Controls.stop(false);
- if (this._undoTyping) {
- this._undoTyping.end();
- this._undoTyping = undefined;
- }
- this.doLinkOnDeselect();
-
- // move the richtextmenu offscreen
- if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
- }
-
- _lastTimedMark: Mark | undefined = undefined;
- onKeyPress = (e: React.KeyboardEvent) => {
- if (e.altKey) {
- e.preventDefault();
- return;
- }
- const state = this._editorView!.state;
- if (!state.selection.empty && e.key === "%") {
- this._rules!.EnteringStyle = true;
- e.preventDefault();
- e.stopPropagation();
- return;
- }
-
- if (state.selection.empty || !this._rules!.EnteringStyle) {
- this._rules!.EnteringStyle = false;
- }
- if (e.key === "Escape") {
- this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
- (document.activeElement as any).blur?.();
- SelectionManager.DeselectAll();
- }
- e.stopPropagation();
- if (e.key === "Tab" || e.key === "Enter") {
- e.preventDefault();
- }
- const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
- this._lastTimedMark = mark;
- this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
-
- if (!this._undoTyping) {
- this._undoTyping = UndoManager.StartBatch("undoTyping");
- }
- }
-
- onscrolled = (ev: React.UIEvent) => {
- this.props.Document.scrollPos = this._scrollRef.current!.scrollTop;
- }
- @action
- tryUpdateHeight(limitHeight?: number) {
- let scrollHeight = this._ref.current?.scrollHeight;
- if (this.layoutDoc._autoHeight && scrollHeight &&
- getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
- if (limitHeight && scrollHeight > limitHeight) {
- scrollHeight = limitHeight;
- this.layoutDoc.limitHeight = undefined;
- this.layoutDoc._autoHeight = false;
- }
- const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
- const dh = NumCast(this.layoutDoc._height, 0);
- const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
- if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle
- this.layoutDoc._height = newHeight;
- this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
- }
- }
- }
-
- @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
- sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
- sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0);
- @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
- render() {
- TraceMobx();
- const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
- const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
- if (this.props.isSelected()) {
- this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
- } else if (FormattedTextBoxComment.textBox === this) {
- FormattedTextBoxComment.Hide();
- }
- return (
-
- this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)}
- onBlur={this.onBlur}
- onPointerUp={this.onPointerUp}
- onPointerDown={this.onPointerDown}
- onMouseUp={this.onMouseUp}
- onWheel={this.onPointerWheel}
- onPointerEnter={action(() => this._entered = true)}
- onPointerLeave={action((e: React.PointerEvent
) => {
- 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;
- }
- }
- })}
- >
-
- {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
- this.toggleSidebar()} /> :
-
-
-
-
this.toggleSidebar()} />
-
}
- {!this.props.Document._showAudio ? (null) :
-
{
- runInAction(() => this._recording = !this._recording);
- setTimeout(() => this._editorView!.focus(), 500);
- e.stopPropagation();
- }} >
-
-
}
-
- );
- }
-}
diff --git a/src/client/views/nodes/FormattedTextBoxComment.scss b/src/client/views/nodes/FormattedTextBoxComment.scss
deleted file mode 100644
index 2dd63ec21..000000000
--- a/src/client/views/nodes/FormattedTextBoxComment.scss
+++ /dev/null
@@ -1,33 +0,0 @@
-.FormattedTextBox-tooltip {
- position: absolute;
- pointer-events: none;
- z-index: 20;
- background: white;
- border: 1px solid silver;
- border-radius: 2px;
- margin-bottom: 7px;
- -webkit-transform: translateX(-50%);
- transform: translateX(-50%);
- }
- .FormattedTextBox-tooltip:before {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -6px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: silver;
- }
- .FormattedTextBox-tooltip:after {
- content: "";
- height: 0; width: 0;
- position: absolute;
- left: 50%;
- margin-left: -5px;
- bottom: -4.5px;
- border: 5px solid transparent;
- border-bottom-width: 0;
- border-top-color: white;
- }
\ No newline at end of file
diff --git a/src/client/views/nodes/FormattedTextBoxComment.tsx b/src/client/views/nodes/FormattedTextBoxComment.tsx
deleted file mode 100644
index dfea0f6bb..000000000
--- a/src/client/views/nodes/FormattedTextBoxComment.tsx
+++ /dev/null
@@ -1,236 +0,0 @@
-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/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 wiki from "wikijs";
-import { DocumentType } from "../../documents/DocumentTypes";
-
-export let formattedTextBoxCommentPlugin = new Plugin({
- view(editorView) { return new FormattedTextBoxComment(editorView); }
-});
-export function findOtherUserMark(marks: Mark[]): Mark | undefined {
- return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
-}
-export function findUserMark(marks: Mark[]): Mark | undefined {
- return marks.find(m => m.attrs.userid);
-}
-export function findLinkMark(marks: Mark[]): Mark | undefined {
- return marks.find(m => m.type === schema.marks.link);
-}
-export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
- let before = 0;
- let nbef = rpos.nodeBefore;
- while (nbef && finder(nbef.marks)) {
- before += nbef.nodeSize;
- rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
- rpos && (nbef = rpos.nodeBefore);
- }
- return before;
-}
-export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
- let after = 0;
- let naft = rpos.nodeAfter;
- while (naft && finder(naft.marks)) {
- after += naft.nodeSize;
- rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
- rpos && (naft = rpos.nodeAfter);
- }
- return after;
-}
-
-
-export class FormattedTextBoxComment {
- static tooltip: HTMLElement;
- static tooltipText: HTMLElement;
- static tooltipInput: HTMLInputElement;
- static start: number;
- static end: number;
- static mark: Mark;
- static textBox: FormattedTextBox | undefined;
- static linkDoc: Doc | undefined;
- constructor(view: any) {
- if (!FormattedTextBoxComment.tooltip) {
- const root = document.getElementById("root");
- FormattedTextBoxComment.tooltipInput = document.createElement("input");
- FormattedTextBoxComment.tooltipInput.type = "checkbox";
- FormattedTextBoxComment.tooltip = document.createElement("div");
- FormattedTextBoxComment.tooltipText = document.createElement("div");
- FormattedTextBoxComment.tooltipText.style.width = "100%";
- FormattedTextBoxComment.tooltipText.style.height = "100%";
- FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis";
- FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
- FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
- FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
- FormattedTextBoxComment.tooltip.style.maxWidth = "350px";
- FormattedTextBoxComment.tooltip.style.maxHeight = "250px";
- FormattedTextBoxComment.tooltip.style.width = "100%";
- FormattedTextBoxComment.tooltip.style.height = "100%";
- FormattedTextBoxComment.tooltip.style.overflow = "hidden";
- FormattedTextBoxComment.tooltip.style.display = "none";
- FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput);
- FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
- const keep = e.target && (e.target as any).type === "checkbox" ? true : false;
- const textBox = FormattedTextBoxComment.textBox;
- if (FormattedTextBoxComment.linkDoc && !keep && textBox) {
- if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
- textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
- } else {
- DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
- (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
- }
- } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
- textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight");
- }
- keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
- FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
- e.stopPropagation();
- e.preventDefault();
- };
- root && root.appendChild(FormattedTextBoxComment.tooltip);
- }
- }
-
- public static Hide() {
- FormattedTextBoxComment.textBox = undefined;
- FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
- ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
- }
- public static SetState(textBox: any, start: number, end: number, mark: Mark) {
- FormattedTextBoxComment.textBox = textBox;
- FormattedTextBoxComment.start = start;
- FormattedTextBoxComment.end = end;
- FormattedTextBoxComment.mark = mark;
- FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
- }
-
- static update(view: EditorView, lastState?: EditorState) {
- const state = view.state;
- // Don't do anything if the document/selection didn't change
- if (lastState && lastState.doc.eq(state.doc) &&
- lastState.selection.eq(state.selection)) {
- return;
- }
- FormattedTextBoxComment.linkDoc = undefined;
-
- const textBox = FormattedTextBoxComment.textBox;
- if (!textBox || !textBox.props) {
- return;
- }
- let set = "none";
- let nbef = 0;
- FormattedTextBoxComment.tooltipInput.style.display = "none";
- FormattedTextBoxComment.tooltip.style.width = "";
- FormattedTextBoxComment.tooltip.style.height = "";
- (FormattedTextBoxComment.tooltipText as any).href = "";
- FormattedTextBoxComment.tooltipText.style.whiteSpace = "";
- FormattedTextBoxComment.tooltipText.style.overflow = "";
- // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
- if (state.selection.$from) {
- nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
- const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
- const noselection = view.state.selection.$from === view.state.selection.$to;
- let child: any = null;
- state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
- const mark = child && findOtherUserMark(child.marks);
- if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
- FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
- }
- if (mark && child && ((nbef && naft) || !noselection)) {
- FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString();
- set = "";
- FormattedTextBoxComment.tooltipInput.style.display = "";
- }
- }
- // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links.
- if (set === "none" && state.selection.$from) {
- nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
- const naft = findEndOfMark(state.selection.$from, view, findLinkMark);
- let child: any = null;
- state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
- const mark = child && findLinkMark(child.marks);
- if (mark && child && nbef && naft && mark.attrs.showPreview) {
- FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href;
- (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
- if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) {
- wiki().page(mark.attrs.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500)));
- } else {
- FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre";
- FormattedTextBoxComment.tooltipText.style.overflow = "hidden";
- }
- if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
- FormattedTextBoxComment.tooltipText.textContent = "target not found...";
- (FormattedTextBoxComment.tooltipText as any).href = "";
- const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
- try {
- ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
- } catch (e) { }
- docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => {
- if (linkDoc instanceof Doc) {
- (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
- FormattedTextBoxComment.linkDoc = linkDoc;
- const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
- const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
- if (anchor !== target && anchor && target) {
- target.scrollY = NumCast(anchor?.y);
- }
- if (target) {
- ReactDOM.render(
Math.min(350, NumCast(target._width, 350))}
- PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
- focus={emptyFunction}
- whenActiveChanged={returnFalse}
- />, FormattedTextBoxComment.tooltipText);
- FormattedTextBoxComment.tooltip.style.width = NumCast(target.width) ? `${NumCast(target.width)}` : "100%";
- FormattedTextBoxComment.tooltip.style.height = NumCast(target.height) ? `${NumCast(target.height)}` : "100%";
- }
- // let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document ....
- // let text = ext && StrCast(ext.text);
- // ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title)));
- }
- });
- }
- set = "";
- }
- }
- if (set !== "none") {
- // These are in screen coordinates
- // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
- const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
- // The box in which the tooltip is positioned, to use as base
- const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect();
- // Find a center-ish x position from the selection endpoints (when
- // crossing lines, end may be more to the left)
- const left = Math.max((start.left + end.left) / 2, start.left + 3);
- FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
- FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
- }
- FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
- }
-
- destroy() { }
-}
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{
+ 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 (
+
+
+
+ );
+ }
+}
\ 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 {
+
+ _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 ;
+ // 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 (
+
+
+
+
+
+
+ );
+
+ }
+ }
+
+}
\ 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(, 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 {
+ _reactionDisposer: IReactionDisposer | undefined;
+ _textBoxDoc: Doc;
+ _fieldKey: string;
+ _fieldStringRef = React.createRef();
+ @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 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 {
+ 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}
+ ;
+ }
+ }
+ }
+
+ // 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();
+ }
+ 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
+
+ {this._fieldKey}
+
+
+
+ {this.fieldValueContent}
+
+
+ {!this._showEnumerables ? (null) :
}
+
+
;
+ }
+}
\ 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 {
+ _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 (
+
+ );
+ }
+}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss
new file mode 100644
index 000000000..477a2ca08
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss
@@ -0,0 +1,265 @@
+@import "../../globalCssVariables";
+
+.ProseMirror {
+ width: 100%;
+ height: 100%;
+ min-height: 100%;
+}
+
+.ProseMirror:focus {
+ outline: none !important;
+}
+
+.formattedTextBox-cont {
+ touch-action: none;
+ cursor: text;
+ background: inherit;
+ padding: 0;
+ border-width: 0px;
+ border-radius: inherit;
+ border-color: $intermediate-color;
+ box-sizing: border-box;
+ background-color: inherit;
+ border-style: solid;
+ overflow-y: auto;
+ overflow-x: hidden;
+ color: initial;
+ max-height: 100%;
+ display: flex;
+ flex-direction: row;
+ transition: opacity 1s;
+
+ .formattedTextBox-dictation {
+ height: 12px;
+ width: 10px;
+ top: 0px;
+ left: 0px;
+ position: absolute;
+ }
+}
+.formattedTextBox-outer {
+ position: relative;
+ overflow: auto;
+ display: inline-block;
+ width: 100%;
+ height: 100%;
+}
+
+.formattedTextBox-sidebar-handle {
+ position: absolute;
+ top: calc(50% - 17.5px);
+ width: 10px;
+ height: 35px;
+ background: lightgray;
+ border-radius: 20px;
+ cursor:grabbing;
+}
+
+.formattedTextBox-cont>.formattedTextBox-sidebar-handle {
+ right: 0;
+ left: unset;
+}
+
+.formattedTextBox-sidebar,
+.formattedTextBox-sidebar-inking {
+ border-left: dashed 1px black;
+ height: 100%;
+ display: inline-block;
+ position: absolute;
+ right: 0;
+
+ .collectionfreeformview-container {
+ position: relative;
+ }
+
+ >.formattedTextBox-sidebar-handle {
+ right: unset;
+ left: -5;
+ }
+}
+
+.formattedTextBox-sidebar-inking {
+ pointer-events: all;
+}
+
+.formattedTextBox-inner-rounded {
+ height: 70%;
+ width: 85%;
+ position: absolute;
+ overflow: auto;
+ top: 15%;
+ left: 10%;
+}
+
+.formattedTextBox-inner-rounded,
+.formattedTextBox-inner {
+ height: 100%;
+ white-space: pre-wrap;
+}
+
+// .menuicon {
+// display: inline-block;
+// border-right: 1px solid rgba(0, 0, 0, 0.2);
+// color: #888;
+// line-height: 1;
+// padding: 0 7px;
+// margin: 1px;
+// cursor: pointer;
+// text-align: center;
+// min-width: 1.4em;
+// }
+
+.strong,
+.heading {
+ font-weight: bold;
+}
+
+.em {
+ font-style: italic;
+}
+
+.userMarkOpen {
+ background: rgba(255, 255, 0, 0.267);
+ display: inline;
+}
+
+.userMark {
+ background: rgba(255, 255, 0, 0.267);
+ font-size: 2px;
+ display: inline-grid;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 10px;
+ min-height: 10px;
+ text-align: center;
+ align-content: center;
+}
+
+footnote {
+ display: inline-block;
+ position: relative;
+ cursor: pointer;
+
+ div {
+ padding: 0 !important;
+ }
+}
+
+footnote::after {
+ content: counter(prosemirror-footnote);
+ vertical-align: super;
+ font-size: 75%;
+ counter-increment: prosemirror-footnote;
+}
+
+.ProseMirror {
+ counter-reset: prosemirror-footnote;
+}
+
+.footnote-tooltip {
+ cursor: auto;
+ font-size: 75%;
+ position: absolute;
+ left: -30px;
+ top: calc(100% + 10px);
+ background: silver;
+ padding: 3px;
+ border-radius: 2px;
+ max-width: 100px;
+ min-width: 50px;
+ width: max-content;
+}
+
+.prosemirror-attribution {
+ font-size: 8px;
+}
+
+.footnote-tooltip::before {
+ border: 5px solid silver;
+ border-top-width: 0px;
+ border-left-color: transparent;
+ border-right-color: transparent;
+ position: absolute;
+ top: -5px;
+ left: 27px;
+ content: " ";
+ height: 0;
+ width: 0;
+}
+
+
+.formattedTextBox-inlineComment {
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::before {
+ content: "→";
+ }
+ &:hover {
+ background: orange;
+ }
+}
+
+.formattedTextBox-summarizer {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "←";
+ }
+}
+
+.formattedTextBox-summarizer-collapsed {
+ opacity: 0.5;
+ position: relative;
+ width: 40px;
+ height: 20px;
+ &::after {
+ content: "...";
+ }
+}
+
+.ProseMirror {
+ touch-action: none;
+ span {
+ font-family: inherit;
+ }
+
+ ol, ul {
+ counter-reset: deci1 0 multi1 0;
+ padding-left: 1em;
+ font-family: inherit;
+ }
+ ol {
+ margin-left: 1em;
+ font-family: inherit;
+ }
+
+ .decimal1-ol { counter-reset: deci1; p {display: inline; font-family: inherit} margin-left: 0; }
+ .decimal2-ol { counter-reset: deci2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1em;}
+ .decimal3-ol { counter-reset: deci3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .decimal4-ol { counter-reset: deci4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3em;}
+ .decimal5-ol { counter-reset: deci5; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal6-ol { counter-reset: deci6; p {display: inline; font-family: inherit} font-size: smaller; }
+ .decimal7-ol { counter-reset: deci7; p {display: inline; font-family: inherit} font-size: smaller; }
+
+ .multi1-ol { counter-reset: multi1; p {display: inline; font-family: inherit} margin-left: 0; padding-left: 1.2em }
+ .multi2-ol { counter-reset: multi2; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 1.4em;}
+ .multi3-ol { counter-reset: multi3; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 2em;}
+ .multi4-ol { counter-reset: multi4; p {display: inline; font-family: inherit} font-size: smaller; padding-left: 3.4em;}
+
+ .decimal1:before { transition: 0.5s;counter-increment: deci1; display: inline-block; margin-left: -1em; width: 1em; content: counter(deci1) ". "; }
+ .decimal2:before { transition: 0.5s;counter-increment: deci2; display: inline-block; margin-left: -2.1em; width: 2.1em; content: counter(deci1) "."counter(deci2) ". "; }
+ .decimal3:before { transition: 0.5s;counter-increment: deci3; display: inline-block; margin-left: -2.85em;width: 2.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) ". "; }
+ .decimal4:before { transition: 0.5s;counter-increment: deci4; display: inline-block; margin-left: -3.85em;width: 3.85em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) ". "; }
+ .decimal5:before { transition: 0.5s;counter-increment: deci5; display: inline-block; margin-left: -2em; width: 5em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) ". "; }
+ .decimal6:before { transition: 0.5s;counter-increment: deci6; display: inline-block; margin-left: -2em; width: 6em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) ". "; }
+ .decimal7:before { transition: 0.5s;counter-increment: deci7; display: inline-block; margin-left: -2em; width: 7em; content: counter(deci1) "."counter(deci2) "."counter(deci3) "."counter(deci4) "."counter(deci5) "."counter(deci6) "."counter(deci7) ". "; }
+
+ .multi1:before { transition: 0.5s;counter-increment: multi1; display: inline-block; margin-left: -1em; width: 1.2em; content: counter(multi1, upper-alpha) ". "; }
+ .multi2:before { transition: 0.5s;counter-increment: multi2; display: inline-block; margin-left: -2em; width: 2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) ". "; }
+ .multi3:before { transition: 0.5s;counter-increment: multi3; display: inline-block; margin-left: -2.85em; width:2.85em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) ". "; }
+ .multi4:before { transition: 0.5s;counter-increment: multi4; display: inline-block; margin-left: -4.2em; width: 4.2em; content: counter(multi1, upper-alpha) "."counter(multi2, decimal) "."counter(multi3, lower-alpha) "."counter(multi4, lower-roman) ". "; }
+}
\ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
new file mode 100644
index 000000000..248b4f467
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -0,0 +1,1303 @@
+import { library } from '@fortawesome/fontawesome-svg-core';
+import { faEdit, faSmile, faTextHeight, faUpload } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { isEqual } from "lodash";
+import { action, computed, IReactionDisposer, Lambda, observable, reaction, runInAction } from "mobx";
+import { observer } from "mobx-react";
+import { baseKeymap } from "prosemirror-commands";
+import { history } from "prosemirror-history";
+import { inputRules } from 'prosemirror-inputrules';
+import { keymap } from "prosemirror-keymap";
+import { Fragment, 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 "./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");
+
+library.add(faEdit);
+library.add(faSmile, faTextHeight, faUpload);
+
+export interface FormattedTextBoxProps {
+ hideOnLeave?: boolean;
+ makeLink?: () => Opt;
+ xMargin?: number;
+ yMargin?: number;
+}
+
+const richTextSchema = createSchema({
+ documentText: "string"
+});
+
+export const GoogleRef = "googleDocId";
+
+type RichTextDocument = makeInterface<[typeof richTextSchema, typeof documentSchema]>;
+const RichTextDocument = makeInterface(richTextSchema, documentSchema);
+
+type PullHandler = (exportState: Opt, dataDoc: Doc) => void;
+
+@observer
+export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProps & FormattedTextBoxProps), RichTextDocument>(RichTextDocument) {
+ public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); }
+ public static blankState = () => EditorState.create(FormattedTextBox.Instance.config);
+ public static Instance: FormattedTextBox;
+ public ProseRef?: HTMLDivElement;
+ private _ref: React.RefObject = React.createRef();
+ private _scrollRef: React.RefObject = React.createRef();
+ private _editorView: Opt;
+ private _applyingChange: boolean = false;
+ private _searchIndex = 0;
+ private _sidebarMovement = 0;
+ private _lastX = 0;
+ private _lastY = 0;
+ private _undoTyping?: UndoManager.Batch;
+ private _disposers: { [name: string]: IReactionDisposer } = {};
+ private dropDisposer?: DragManager.DragDropDisposer;
+
+ @computed get _recording() { return this.dataDoc.audioState === "recording"; }
+ set _recording(value) { this.dataDoc.audioState = value ? "recording" : undefined; }
+
+ @observable private _entered = false;
+
+ public static FocusedBox: FormattedTextBox | undefined;
+ public static SelectOnLoad = "";
+ public static SelectOnLoadChar = "";
+ public static IsFragment(html: string) {
+ return html.indexOf("data-pm-slice") !== -1;
+ }
+ public static GetHref(html: string): string {
+ const parser = new DOMParser();
+ const parsedHtml = parser.parseFromString(html, 'text/html');
+ if (parsedHtml.body.childNodes.length === 1 && parsedHtml.body.childNodes[0].childNodes.length === 1 &&
+ (parsedHtml.body.childNodes[0].childNodes[0] as any).href) {
+ return (parsedHtml.body.childNodes[0].childNodes[0] as any).href;
+ }
+ return "";
+ }
+ public static GetDocFromUrl(url: string) {
+ if (url.startsWith(document.location.origin)) {
+ const split = new URL(url).pathname.split("doc/");
+ const docid = split[split.length - 1];
+ return docid;
+ }
+ return "";
+ }
+
+ @undoBatch
+ public setFontColor(color: string) {
+ const view = this._editorView!;
+ if (view.state.selection.from === view.state.selection.to) return false;
+ if (view.state.selection.to - view.state.selection.from > view.state.doc.nodeSize - 3) {
+ this.layoutDoc.color = color;
+ }
+ const colorMark = view.state.schema.mark(view.state.schema.marks.pFontColor, { color: color });
+ view.dispatch(view.state.tr.addMark(view.state.selection.from, view.state.selection.to, colorMark));
+ return true;
+ }
+
+ constructor(props: any) {
+ super(props);
+ FormattedTextBox.Instance = this;
+ this.updateHighlights();
+ }
+
+ public get CurrentDiv(): HTMLDivElement { return this._ref.current!; }
+
+ linkOnDeselect: Map = new Map();
+
+ doLinkOnDeselect() {
+ Array.from(this.linkOnDeselect.entries()).map(entry => {
+ const key = entry[0];
+ const value = entry[1];
+ const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+ DocServer.GetRefField(value).then(doc => {
+ DocServer.GetRefField(id).then(linkDoc => {
+ this.dataDoc[key] = doc || Docs.Create.FreeformDocument([], { title: value, _width: 500, _height: 500 }, value);
+ DocUtils.Publish(this.dataDoc[key] as Doc, value, this.props.addDocument, this.props.removeDocument);
+ if (linkDoc) { (linkDoc as Doc).anchor2 = this.dataDoc[key] as Doc; }
+ else DocUtils.MakeLink({ doc: this.props.Document }, { doc: this.dataDoc[key] as Doc }, "link to named target", id);
+ });
+ });
+ });
+ this.linkOnDeselect.clear();
+ }
+
+ dispatchTransaction = (tx: Transaction) => {
+ if (this._editorView) {
+ const metadata = tx.selection.$from.marks().find((m: Mark) => m.type === schema.marks.metadata);
+ if (metadata) {
+ const range = tx.selection.$from.blockRange(tx.selection.$to);
+ let text = range ? tx.doc.textBetween(range.start, range.end) : "";
+ let textEndSelection = tx.selection.to;
+ for (; textEndSelection < range!.end && text[textEndSelection - range!.start] !== " "; textEndSelection++) { }
+ text = text.substr(0, textEndSelection - range!.start);
+ text = text.split(" ")[text.split(" ").length - 1];
+ const split = text.split("::");
+ if (split.length > 1 && split[1]) {
+ const key = split[0];
+ const value = split[split.length - 1];
+ this.linkOnDeselect.set(key, value);
+
+ const id = Utils.GenerateDeterministicGuid(this.dataDoc[Id] + key);
+ const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + id), location: "onRight", title: value });
+ const mval = this._editorView.state.schema.marks.metadataVal.create();
+ const offset = (tx.selection.to === range!.end - 1 ? -1 : 0);
+ tx = tx.addMark(textEndSelection - value.length + offset, textEndSelection, link).addMark(textEndSelection - value.length + offset, textEndSelection, mval);
+ this.dataDoc[key] = value;
+ }
+ }
+ const state = this._editorView.state.apply(tx);
+ this._editorView.updateState(state);
+ (tx.storedMarks && !this._editorView.state.storedMarks) && (this._editorView.state.storedMarks = tx.storedMarks);
+
+ const tsel = this._editorView.state.selection.$from;
+ tsel.marks().filter(m => m.type === this._editorView!.state.schema.marks.user_mark).map(m => AudioBox.SetScrubTime(Math.max(0, m.attrs.modified * 1000)));
+ const curText = state.doc.textBetween(0, state.doc.content.size, " \n");
+ const curTemp = Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField);
+ if (!this._applyingChange) {
+ this._applyingChange = true;
+ this.dataDoc[this.props.fieldKey + "-lastModified"] = new DateField(new Date(Date.now()));
+ if (!curTemp || curText) { // if no template, or there's text, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended)
+ this.dataDoc[this.props.fieldKey] = new RichTextField(JSON.stringify(state.toJSON()), curText);
+ this.dataDoc[this.props.fieldKey + "-noTemplate"] = (curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited
+ } else { // if we've deleted all the text in a note driven by a template, then restore the template data
+ this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse(curTemp.Data)));
+ this.dataDoc[this.props.fieldKey + "-noTemplate"] = undefined; // mark the data field as not being split from any template it might have
+ }
+ this._applyingChange = false;
+ }
+ this.updateTitle();
+ this.tryUpdateHeight();
+ }
+ }
+
+ updateTitle = () => {
+ if ((this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing
+ StrCast(this.dataDoc.title).startsWith("-") && this._editorView && !this.rootDoc.customTitle) {
+ const str = this._editorView.state.doc.textContent;
+ const titlestr = str.substr(0, Math.min(40, str.length));
+ this.dataDoc.title = "-" + titlestr + (str.length > 40 ? "..." : "");
+ }
+ }
+
+ // needs a better API for taking in a set of words with target documents instead of just one target
+ public hyperlinkTerms = (terms: string[], target: Doc) => {
+ if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
+ const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+ const tr = this._editorView.state.tr;
+ const flattened: TextSelection[] = [];
+ res.map(r => r.map(h => flattened.push(h)));
+ const lastSel = Math.min(flattened.length - 1, this._searchIndex);
+ this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ const alink = DocUtils.MakeLink({ doc: this.props.Document }, { doc: target }, "automatic")!;
+ const link = this._editorView.state.schema.marks.link.create({
+ href: Utils.prepend("/doc/" + alink[Id]),
+ title: "a link", location: location, linkId: alink[Id], targetId: target[Id]
+ });
+ this._editorView.dispatch(tr.addMark(flattened[lastSel].from, flattened[lastSel].to, link));
+ }
+ }
+ public highlightSearchTerms = (terms: string[]) => {
+ if (this._editorView && (this._editorView as any).docView && terms.some(t => t)) {
+ const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ const res = terms.filter(t => t).map(term => this.findInNode(this._editorView!, this._editorView!.state.doc, term));
+ let tr = this._editorView.state.tr;
+ const flattened: TextSelection[] = [];
+ res.map(r => r.map(h => flattened.push(h)));
+ const lastSel = Math.min(flattened.length - 1, this._searchIndex);
+ flattened.forEach((h: TextSelection, ind: number) => tr = tr.addMark(h.from, h.to, ind === lastSel ? activeMark : mark));
+ this._searchIndex = ++this._searchIndex > flattened.length - 1 ? 0 : this._searchIndex;
+ this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(flattened[lastSel].from), tr.doc.resolve(flattened[lastSel].to))).scrollIntoView());
+ }
+ }
+
+ public unhighlightSearchTerms = () => {
+ if (this._editorView && (this._editorView as any).docView) {
+ const mark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ const activeMark = this._editorView.state.schema.mark(this._editorView.state.schema.marks.search_highlight, { selected: true });
+ const end = this._editorView.state.doc.nodeSize - 2;
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(0, end, mark).removeMark(0, end, activeMark));
+ }
+ }
+ adoptAnnotation = (start: number, end: number, mark: Mark) => {
+ const view = this._editorView!;
+ const nmark = view.state.schema.marks.user_mark.create({ ...mark.attrs, userid: Doc.CurrentUserEmail });
+ view.dispatch(view.state.tr.removeMark(start, end, nmark).addMark(start, end, nmark));
+ }
+ protected createDropTarget = (ele: HTMLDivElement) => {
+ this.ProseRef = ele;
+ this.dropDisposer?.();
+ ele && (this.dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.props.Document));
+ }
+
+ @undoBatch
+ @action
+ drop = async (e: Event, de: DragManager.DropEvent) => {
+ if (de.complete.docDragData) {
+ const draggedDoc = de.complete.docDragData.draggedDocuments.length && de.complete.docDragData.draggedDocuments[0];
+ // replace text contents whend dragging with Alt
+ if (draggedDoc && draggedDoc.type === DocumentType.RTF && !Doc.AreProtosEqual(draggedDoc, this.props.Document) && de.altKey) {
+ if (draggedDoc.data instanceof RichTextField) {
+ Doc.GetProto(this.dataDoc)[this.props.fieldKey] = new RichTextField(draggedDoc.data.Data, draggedDoc.data.Text);
+ e.stopPropagation();
+ }
+ // embed document when dragging with a userDropAction or an embedDoc flag set
+ } else if (de.complete.docDragData.userDropAction || de.complete.docDragData.embedDoc) {
+ const target = de.complete.docDragData.droppedDocuments[0];
+ // const link = DocUtils.MakeLink({ doc: this.dataDoc, ctx: this.props.ContainingCollectionDoc }, { doc: target }, "Embedded Doc:" + target.title);
+ // if (link) {
+ target._fitToBox = true;
+ const node = schema.nodes.dashDoc.create({
+ width: target[WidthSym](), height: target[HeightSym](),
+ title: "dashDoc", docid: target[Id],
+ float: "right"
+ });
+ const view = this._editorView!;
+ view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node));
+ this.tryUpdateHeight();
+ e.stopPropagation();
+ // }
+ } // otherwise, fall through to outer collection to handle drop
+ } else if (de.complete.linkDragData) {
+ de.complete.linkDragData.linkDropCallback = this.linkDrop;
+ }
+ }
+ linkDrop = (data: DragManager.LinkDragData) => {
+ const linkDoc = data.linkDocument!;
+ const anchor1Title = linkDoc.anchor1 instanceof Doc ? StrCast(linkDoc.anchor1.title) : "-untitled-";
+ const anchor1Id = linkDoc.anchor1 instanceof Doc ? linkDoc.anchor1[Id] : "";
+ this.makeLinkToSelection(linkDoc[Id], anchor1Title, "onRight", anchor1Id);
+ }
+
+ getNodeEndpoints(context: Node, node: Node): { from: number, to: number } | null {
+ let offset = 0;
+
+ if (context === node) return { from: offset, to: offset + node.nodeSize };
+
+ if (node.isBlock) {
+ // tslint:disable-next-line: prefer-for-of
+ for (let i = 0; i < (context.content as any).content.length; i++) {
+ const result = this.getNodeEndpoints((context.content as any).content[i], node);
+ if (result) {
+ return {
+ from: result.from + offset + (context.type.name === "doc" ? 0 : 1),
+ to: result.to + offset + (context.type.name === "doc" ? 0 : 1)
+ };
+ }
+ offset += (context.content as any).content[i].nodeSize;
+ }
+ return null;
+ } else {
+ return null;
+ }
+ }
+
+
+ //Recursively finds matches within a given node
+ findInNode(pm: EditorView, node: Node, find: string) {
+ let ret: TextSelection[] = [];
+
+ if (node.isTextblock) {
+ let index = 0, foundAt;
+ const ep = this.getNodeEndpoints(pm.state.doc, node);
+ while (ep && (foundAt = node.textContent.slice(index).search(RegExp(find, "i"))) > -1) {
+ const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + foundAt + 1), pm.state.doc.resolve(ep.from + index + foundAt + find.length + 1));
+ ret.push(sel);
+ index = index + foundAt + find.length;
+ }
+ } else {
+ node.content.forEach((child, i) => ret = ret.concat(this.findInNode(pm, child, find)));
+ }
+ return ret;
+ }
+ static _highlights: string[] = ["Text from Others", "Todo Items", "Important Items", "Disagree Items", "Ignore Items"];
+
+ updateHighlights = () => {
+ clearStyleSheetRules(FormattedTextBox._userStyleSheet);
+ if (FormattedTextBox._highlights.indexOf("Text from Others") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-remote", { background: "yellow" });
+ }
+ if (FormattedTextBox._highlights.indexOf("My Text") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { background: "moccasin" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Todo Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "todo", { outline: "black solid 1px" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Important Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "important", { "font-size": "larger" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Disagree Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "disagree", { "text-decoration": "line-through" });
+ }
+ if (FormattedTextBox._highlights.indexOf("Ignore Items") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "userTag-" + "ignore", { "font-size": "1" });
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Minute") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ const min = Math.round(Date.now() / 1000 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-min-" + (min - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ setTimeout(() => this.updateHighlights());
+ }
+ if (FormattedTextBox._highlights.indexOf("By Recent Hour") !== -1) {
+ addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-" + Doc.CurrentUserEmail.replace(".", "").replace("@", ""), { opacity: "0.1" });
+ const hr = Math.round(Date.now() / 1000 / 60 / 60);
+ numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, "UM-hr-" + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
+ }
+ }
+
+ sidebarDown = (e: React.PointerEvent) => {
+ this._lastX = e.clientX;
+ this._lastY = e.clientY;
+ this._sidebarMovement = 0;
+ document.addEventListener("pointermove", this.sidebarMove);
+ document.addEventListener("pointerup", this.sidebarUp);
+ e.stopPropagation();
+ e.preventDefault(); // prevents text from being selected during drag
+ }
+ sidebarMove = (e: PointerEvent) => {
+ const bounds = this.CurrentDiv.getBoundingClientRect();
+ this._sidebarMovement += Math.sqrt((e.clientX - this._lastX) * (e.clientX - this._lastX) + (e.clientY - this._lastY) * (e.clientY - this._lastY));
+ this.props.Document.sidebarWidthPercent = "" + 100 * (1 - (e.clientX - bounds.left) / bounds.width) + "%";
+ }
+ sidebarUp = (e: PointerEvent) => {
+ document.removeEventListener("pointermove", this.sidebarMove);
+ document.removeEventListener("pointerup", this.sidebarUp);
+ }
+
+ toggleSidebar = () => this._sidebarMovement < 5 && (this.props.Document.sidebarWidthPercent = StrCast(this.props.Document.sidebarWidthPercent, "0%") === "0%" ? "25%" : "0%");
+
+ public static get DefaultLayout(): Doc | string | undefined {
+ return Cast(Doc.UserDoc().defaultTextLayout, Doc, null) || StrCast(Doc.UserDoc().defaultTextLayout, null);
+ }
+ specificContextMenu = (e: React.MouseEvent): void => {
+ const cm = ContextMenu.Instance;
+
+ const funcs: ContextMenuProps[] = [];
+ this.props.Document.isTemplateDoc && funcs.push({ description: "Make Default Layout", event: async () => Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.props.Document), icon: "eye" });
+ funcs.push({ description: "Reset Default Layout", event: () => Doc.UserDoc().defaultTextLayout = undefined, icon: "eye" });
+ !this.props.Document.rootDocument && funcs.push({
+ description: "Make Template", event: () => {
+ this.props.Document.isTemplateDoc = makeTemplate(this.props.Document);
+ Doc.AddDocToList(Cast(Doc.UserDoc()["template-notes"], Doc, null), "data", this.props.Document);
+ }, icon: "eye"
+ });
+ funcs.push({ description: "Toggle Single Line", event: () => this.props.Document._singleLine = !this.props.Document._singleLine, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Sidebar", event: () => this.props.Document._showSidebar = !this.props.Document._showSidebar, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Dictation Icon", event: () => this.props.Document._showAudio = !this.props.Document._showAudio, icon: "expand-arrows-alt" });
+ funcs.push({ description: "Toggle Menubar", event: () => this.toggleMenubar(), icon: "expand-arrows-alt" });
+
+ const highlighting: ContextMenuProps[] = [];
+ ["My Text", "Text from Others", "Todo Items", "Important Items", "Ignore Items", "Disagree Items", "By Recent Minute", "By Recent Hour"].forEach(option =>
+ highlighting.push({
+ description: (FormattedTextBox._highlights.indexOf(option) === -1 ? "Highlight " : "Unhighlight ") + option, event: () => {
+ e.stopPropagation();
+ if (FormattedTextBox._highlights.indexOf(option) === -1) {
+ FormattedTextBox._highlights.push(option);
+ } else {
+ FormattedTextBox._highlights.splice(FormattedTextBox._highlights.indexOf(option), 1);
+ }
+ this.updateHighlights();
+ }, icon: "expand-arrows-alt"
+ }));
+ funcs.push({ description: "highlighting...", subitems: highlighting, icon: "hand-point-right" });
+
+ ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" });
+
+ const change = cm.findByDescription("Change Perspective...");
+ const changeItems: ContextMenuProps[] = change && "subitems" in change ? change.subitems : [];
+
+ const noteTypesDoc = Cast(Doc.UserDoc()["template-notes"], Doc, null);
+ DocListCast(noteTypesDoc?.data).forEach(note => {
+ changeItems.push({
+ description: StrCast(note.title), event: undoBatch(() => {
+ Doc.setNativeView(this.props.Document);
+ Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note);
+ }), icon: "eye"
+ });
+ });
+ changeItems.push({ description: "FreeForm", event: undoBatch(() => Doc.makeCustomViewClicked(this.rootDoc, Docs.Create.FreeformDocument, "freeform"), "change view"), icon: "eye" });
+ !change && cm.addItem({ description: "Change Perspective...", subitems: changeItems, icon: "external-link-alt" });
+
+ const open = cm.findByDescription("Add a Perspective...");
+ const openItems: ContextMenuProps[] = open && "subitems" in open ? open.subitems : [];
+
+ openItems.push({
+ description: "FreeForm", event: undoBatch(() => {
+ const alias = Doc.MakeAlias(this.rootDoc);
+ Doc.makeCustomViewClicked(alias, Docs.Create.FreeformDocument, "freeform");
+ this.props.addDocTab(alias, "onRight");
+ }), icon: "eye"
+ });
+ !open && cm.addItem({ description: "Add a Perspective...", subitems: openItems, icon: "external-link-alt" });
+
+ }
+
+ recordDictation = () => {
+ DictationManager.Controls.listen({
+ interimHandler: this.setCurrentBulletContent,
+ continuous: { indefinite: false },
+ }).then(results => {
+ if (results && [DictationManager.Controls.Infringed].includes(results)) {
+ DictationManager.Controls.stop();
+ }
+ //this._editorView!.focus();
+ });
+ }
+ stopDictation = (abort: boolean) => { DictationManager.Controls.stop(!abort); };
+
+ @action
+ toggleMenubar = () => {
+ this.props.Document._chromeStatus = this.props.Document._chromeStatus === "disabled" ? "enabled" : "disabled";
+ }
+
+ recordBullet = async () => {
+ const completedCue = "end session";
+ const results = await DictationManager.Controls.listen({
+ interimHandler: this.setCurrentBulletContent,
+ continuous: { indefinite: false },
+ terminators: [completedCue, "bullet", "next"]
+ });
+ if (results && [DictationManager.Controls.Infringed, completedCue].includes(results)) {
+ DictationManager.Controls.stop();
+ return;
+ }
+ this.nextBullet(this._editorView!.state.selection.to);
+ setTimeout(this.recordBullet, 2000);
+ }
+
+ setCurrentBulletContent = (value: string) => {
+ if (this._editorView) {
+ const state = this._editorView.state;
+ const now = Date.now();
+ let mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(now / 1000) });
+ if (!this._break && state.selection.to !== state.selection.from) {
+ for (let i = state.selection.from; i <= state.selection.to; i++) {
+ const pos = state.doc.resolve(i);
+ const um = Array.from(pos.marks()).find(m => m.type === schema.marks.user_mark);
+ if (um) {
+ mark = um;
+ break;
+ }
+ }
+ }
+ const recordingStart = DateCast(this.props.Document.recordingStart).date.getTime();
+ this._break = false;
+ value = "" + (mark.attrs.modified * 1000 - recordingStart) / 1000 + value;
+ const from = state.selection.from;
+ const inserted = state.tr.insertText(value).addMark(from, from + value.length + 1, mark);
+ this._editorView.dispatch(inserted.setSelection(TextSelection.create(inserted.doc, from, from + value.length + 1)));
+ }
+ }
+
+ nextBullet = (pos: number) => {
+ if (this._editorView) {
+ const frag = Fragment.fromArray(this.newListItems(2));
+ if (this._editorView.state.doc.resolve(pos).depth >= 2) {
+ const slice = new Slice(frag, 2, 2);
+ let state = this._editorView.state;
+ this._editorView.dispatch(state.tr.step(new ReplaceStep(pos, pos, slice)));
+ pos += 4;
+ state = this._editorView.state;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos, pos)));
+ }
+ }
+ }
+
+ private newListItems = (count: number) => {
+ return numberRange(count).map(x => schema.nodes.list_item.create(undefined, schema.nodes.paragraph.create()));
+ }
+
+ _keymap: any = undefined;
+ _rules: RichTextRules | undefined;
+ @computed get config() {
+ this._keymap = buildKeymap(schema, this.props);
+ this._rules = new RichTextRules(this.props.Document, this);
+ return {
+ schema,
+ plugins: [
+ inputRules(this._rules.inpRules),
+ this.richTextMenuPlugin(),
+ history(),
+ keymap(this._keymap),
+ keymap(baseKeymap),
+ new Plugin({
+ props: {
+ attributes: { class: "ProseMirror-example-setup-style" }
+ }
+ }),
+ formattedTextBoxCommentPlugin
+ ]
+ };
+ }
+
+ makeLinkToSelection(linkDocId: string, title: string, location: string, targetDocId: string) {
+ if (this._editorView) {
+ const link = this._editorView.state.schema.marks.link.create({ href: Utils.prepend("/doc/" + linkDocId), title: title, location: location, linkId: linkDocId, targetId: targetDocId });
+ this._editorView.dispatch(this._editorView.state.tr.removeMark(this._editorView.state.selection.from, this._editorView.state.selection.to, this._editorView.state.schema.marks.link).
+ addMark(this._editorView.state.selection.from, this._editorView.state.selection.to, link));
+ }
+ }
+ componentDidMount() {
+ this._disposers.buttonBar = reaction(
+ () => DocumentButtonBar.Instance,
+ instance => {
+ if (instance) {
+ this.pullFromGoogleDoc(this.checkState);
+ this.dataDoc[GoogleRef] && this.dataDoc.unchanged && runInAction(() => instance.isAnimatingFetch = true);
+ }
+ }
+ );
+ this._disposers.linkMaker = reaction(
+ () => this.props.makeLink?.(),
+ (linkDoc: Opt) => {
+ if (linkDoc) {
+ const anchor2Title = linkDoc.anchor2 instanceof Doc ? StrCast(linkDoc.anchor2.title) : "-untitled-";
+ const anchor2Id = linkDoc.anchor2 instanceof Doc ? linkDoc.anchor2[Id] : "";
+ this.makeLinkToSelection(linkDoc[Id], anchor2Title, "onRight", anchor2Id);
+ }
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.editorState = reaction(
+ () => {
+ if (this.dataDoc[this.props.fieldKey + "-noTemplate"] || !this.props.Document[this.props.fieldKey + "-textTemplate"]) {
+ return Cast(this.dataDoc[this.props.fieldKey], RichTextField, null)?.Data;
+ }
+ return Cast(this.props.Document[this.props.fieldKey + "-textTemplate"], RichTextField, null)?.Data;
+ },
+ incomingValue => {
+ if (incomingValue !== undefined && this._editorView && !this._applyingChange) {
+ const updatedState = JSON.parse(incomingValue);
+ this._editorView.updateState(EditorState.fromJSON(this.config, updatedState));
+ this.tryUpdateHeight();
+ }
+ }
+ );
+ this._disposers.pullDoc = reaction(
+ () => this.props.Document[Pulls],
+ () => {
+ if (!DocumentButtonBar.hasPulledHack) {
+ DocumentButtonBar.hasPulledHack = true;
+ const unchanged = this.dataDoc.unchanged;
+ this.pullFromGoogleDoc(unchanged ? this.checkState : this.updateState);
+ }
+ }
+ );
+ this._disposers.pushDoc = reaction(
+ () => this.props.Document[Pushes],
+ () => {
+ if (!DocumentButtonBar.hasPushedHack) {
+ DocumentButtonBar.hasPushedHack = true;
+ this.pushToGoogleDoc();
+ }
+ }
+ );
+ this._disposers.height = reaction(
+ () => [this.layoutDoc[WidthSym](), this.layoutDoc._autoHeight],
+ () => this.tryUpdateHeight()
+ );
+
+ this.setupEditor(this.config, this.props.fieldKey);
+
+ this._disposers.search = reaction(() => this.rootDoc.searchMatch,
+ search => search ? this.highlightSearchTerms([Doc.SearchQuery()]) : this.unhighlightSearchTerms(),
+ { fireImmediately: true });
+
+ this._disposers.record = reaction(() => this._recording,
+ () => {
+ if (this._recording) {
+ setTimeout(action(() => {
+ this.stopDictation(true);
+ setTimeout(() => this.recordDictation(), 500);
+ }), 500);
+ } else setTimeout(() => this.stopDictation(true), 0);
+ }
+ );
+ this._disposers.scrollToRegion = reaction(
+ () => StrCast(this.layoutDoc.scrollToLinkID),
+ async (scrollToLinkID) => {
+ const findLinkFrag = (frag: Fragment, editor: EditorView) => {
+ const nodes: Node[] = [];
+ frag.forEach((node, index) => {
+ const examinedNode = findLinkNode(node, editor);
+ if (examinedNode && examinedNode.textContent) {
+ nodes.push(examinedNode);
+ start += index;
+ }
+ });
+ return { frag: Fragment.fromArray(nodes), start: start };
+ };
+ const findLinkNode = (node: Node, editor: EditorView) => {
+ if (!node.isText) {
+ const content = findLinkFrag(node.content, editor);
+ return node.copy(content.frag);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type === editor.state.schema.marks.link);
+ return linkIndex !== -1 && scrollToLinkID === marks[linkIndex].attrs.href.replace(/.*\/doc\//, "") ? node : undefined;
+ };
+
+ let start = -1;
+ if (this._editorView && scrollToLinkID) {
+ const editor = this._editorView;
+ const ret = findLinkFrag(editor.state.doc.content, editor);
+
+ if (ret.frag.size > 2 && ret.start >= 0) {
+ let selection = TextSelection.near(editor.state.doc.resolve(ret.start)); // default to near the start
+ if (ret.frag.firstChild) {
+ selection = TextSelection.between(editor.state.doc.resolve(ret.start), editor.state.doc.resolve(ret.start + ret.frag.firstChild.nodeSize)); // bcz: looks better to not have the target selected
+ }
+ editor.dispatch(editor.state.tr.setSelection(new TextSelection(selection.$from, selection.$from)).scrollIntoView());
+ const mark = editor.state.schema.mark(this._editorView.state.schema.marks.search_highlight);
+ setTimeout(() => editor.dispatch(editor.state.tr.addMark(selection.from, selection.to, mark)), 0);
+ setTimeout(() => this.unhighlightSearchTerms(), 2000);
+ }
+ Doc.SetInPlace(this.layoutDoc, "scrollToLinkID", undefined, false);
+ }
+
+ },
+ { fireImmediately: true }
+ );
+ this._disposers.scroll = reaction(() => NumCast(this.props.Document.scrollPos),
+ pos => this._scrollRef.current && this._scrollRef.current.scrollTo({ top: pos }), { fireImmediately: true }
+ );
+
+ setTimeout(() => this.tryUpdateHeight(NumCast(this.layoutDoc.limitHeight, 0)));
+ }
+
+ pushToGoogleDoc = async () => {
+ this.pullFromGoogleDoc(async (exportState: Opt, dataDoc: Doc) => {
+ const modes = GoogleApiClientUtils.Docs.WriteMode;
+ let mode = modes.Replace;
+ let reference: Opt = Cast(this.dataDoc[GoogleRef], "string");
+ if (!reference) {
+ mode = modes.Insert;
+ reference = { title: StrCast(this.dataDoc.title) };
+ }
+ const redo = async () => {
+ if (this._editorView && reference) {
+ const content = await RichTextUtils.GoogleDocs.Export(this._editorView.state);
+ const response = await GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ response && (this.dataDoc[GoogleRef] = response.documentId);
+ const pushSuccess = response !== undefined && !("errors" in response);
+ dataDoc.unchanged = pushSuccess;
+ DocumentButtonBar.Instance.startPushOutcome(pushSuccess);
+ }
+ };
+ const undo = () => {
+ if (!exportState) {
+ return;
+ }
+ const content: GoogleApiClientUtils.Docs.Content = {
+ text: exportState.text,
+ requests: []
+ };
+ if (reference && content) {
+ GoogleApiClientUtils.Docs.write({ reference, content, mode });
+ }
+ };
+ UndoManager.AddEvent({ undo, redo });
+ redo();
+ });
+ }
+
+ pullFromGoogleDoc = async (handler: PullHandler) => {
+ const dataDoc = this.dataDoc;
+ const documentId = StrCast(dataDoc[GoogleRef]);
+ let exportState: Opt;
+ if (documentId) {
+ exportState = await RichTextUtils.GoogleDocs.Import(documentId, dataDoc);
+ }
+ UndoManager.RunInBatch(() => handler(exportState, dataDoc), Pulls);
+ }
+
+ updateState = (exportState: Opt, dataDoc: Doc) => {
+ let pullSuccess = false;
+ if (exportState !== undefined) {
+ pullSuccess = true;
+ dataDoc.data = new RichTextField(JSON.stringify(exportState.state.toJSON()));
+ setTimeout(() => {
+ if (this._editorView) {
+ const state = this._editorView.state;
+ const end = state.doc.content.size - 1;
+ this._editorView.dispatch(state.tr.setSelection(TextSelection.create(state.doc, end, end)));
+ }
+ }, 0);
+ dataDoc.title = exportState.title;
+ this.rootDoc.customTitle = true;
+ dataDoc.unchanged = true;
+ } else {
+ delete dataDoc[GoogleRef];
+ }
+ DocumentButtonBar.Instance.startPullOutcome(pullSuccess);
+ }
+
+ checkState = (exportState: Opt, dataDoc: Doc) => {
+ if (exportState && this._editorView) {
+ const equalContent = isEqual(this._editorView.state.doc, exportState.state.doc);
+ const equalTitles = dataDoc.title === exportState.title;
+ const unchanged = equalContent && equalTitles;
+ dataDoc.unchanged = unchanged;
+ DocumentButtonBar.Instance.setPullState(unchanged);
+ }
+ }
+
+ clipboardTextSerializer = (slice: Slice): string => {
+ let text = "", separated = true;
+ const from = 0, to = slice.content.size;
+ slice.content.nodesBetween(from, to, (node, pos) => {
+ if (node.isText) {
+ text += node.text!.slice(Math.max(from, pos) - pos, to - pos);
+ separated = false;
+ } else if (!separated && node.isBlock) {
+ text += "\n";
+ separated = true;
+ } else if (node.type.name === "hard_break") {
+ text += "\n";
+ }
+ }, 0);
+ return text;
+ }
+
+ sliceSingleNode(slice: Slice) {
+ return slice.openStart === 0 && slice.openEnd === 0 && slice.content.childCount === 1 ? slice.content.firstChild : null;
+ }
+
+ handlePaste = (view: EditorView, event: Event, slice: Slice): boolean => {
+ const cbe = event as ClipboardEvent;
+ const pdfDocId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfOrigin");
+ const pdfRegionId = cbe.clipboardData && cbe.clipboardData.getData("dash/pdfRegion");
+ if (pdfDocId && pdfRegionId) {
+ DocServer.GetRefField(pdfDocId).then(pdfDoc => {
+ DocServer.GetRefField(pdfRegionId).then(pdfRegion => {
+ if ((pdfDoc instanceof Doc) && (pdfRegion instanceof Doc)) {
+ setTimeout(async () => {
+ const targetField = Doc.LayoutFieldKey(pdfDoc);
+ const targetAnnotations = await DocListCastAsync(pdfDoc[DataSym][targetField + "-annotations"]);// bcz: better to have the PDF's view handle updating its own annotations
+ targetAnnotations?.push(pdfRegion);
+ });
+
+ const link = DocUtils.MakeLink({ doc: this.props.Document }, { doc: pdfRegion }, "PDF pasted");
+ if (link) {
+ cbe.clipboardData!.setData("dash/linkDoc", link[Id]);
+ const linkId = link[Id];
+ const frag = addMarkToFrag(slice.content, (node: Node) => addLinkMark(node, StrCast(pdfDoc.title), linkId));
+ slice = new Slice(frag, slice.openStart, slice.openEnd);
+ const tr = view.state.tr.replaceSelection(slice);
+ view.dispatch(tr.scrollIntoView().setMeta("paste", true).setMeta("uiEvent", "paste"));
+ }
+ }
+ });
+ });
+ return true;
+ }
+ return false;
+
+
+ function addMarkToFrag(frag: Fragment, marker: (node: Node) => Node) {
+ const nodes: Node[] = [];
+ frag.forEach(node => nodes.push(marker(node)));
+ return Fragment.fromArray(nodes);
+ }
+ function addLinkMark(node: Node, title: string, linkId: string) {
+ if (!node.isText) {
+ const content = addMarkToFrag(node.content, (node: Node) => addLinkMark(node, title, linkId));
+ return node.copy(content);
+ }
+ const marks = [...node.marks];
+ const linkIndex = marks.findIndex(mark => mark.type.name === "link");
+ const link = view.state.schema.mark(view.state.schema.marks.link, { href: `http://localhost:1050/doc/${linkId}`, location: "onRight", title: title, docref: true });
+ marks.splice(linkIndex === -1 ? 0 : linkIndex, 1, link);
+ return node.mark(marks);
+ }
+ }
+
+ private setupEditor(config: any, fieldKey: string) {
+ const curText = Cast(this.dataDoc[this.props.fieldKey], RichTextField, null);
+ const useTemplate = !curText?.Text && this.props.Document[this.props.fieldKey + "-textTemplate"];
+ const rtfField = Cast((useTemplate && this.props.Document[this.props.fieldKey + "-textTemplate"]) || this.dataDoc[fieldKey], RichTextField);
+ if (this.ProseRef) {
+ const self = this;
+ this._editorView?.destroy();
+ this._editorView = new EditorView(this.ProseRef, {
+ state: rtfField?.Data ? EditorState.fromJSON(config, JSON.parse(rtfField.Data)) : EditorState.create(config),
+ handleScrollToSelection: (editorView) => {
+ const docPos = editorView.coordsAtPos(editorView.state.selection.from);
+ const viewRect = self._ref.current!.getBoundingClientRect();
+ if (docPos.top < viewRect.top || docPos.top > viewRect.bottom) {
+ docPos && (self._scrollRef.current!.scrollTop += (docPos.top - viewRect.top) * self.props.ScreenToLocalTransform().Scale);
+ }
+ return true;
+ },
+ dispatchTransaction: this.dispatchTransaction,
+ nodeViews: {
+ 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); },
+ // 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); }
+ },
+ clipboardTextSerializer: this.clipboardTextSerializer,
+ handlePaste: this.handlePaste,
+ });
+ const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
+ if (startupText) {
+ const { state: { tr }, dispatch } = this._editorView;
+ dispatch(tr.insertText(startupText));
+ }
+ }
+
+ const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad;
+ if (selectOnLoad && !this.props.dontRegisterView) {
+ FormattedTextBox.SelectOnLoad = "";
+ this.props.select(false);
+ FormattedTextBox.SelectOnLoadChar && this._editorView!.dispatch(this._editorView!.state.tr.insertText(FormattedTextBox.SelectOnLoadChar));
+ FormattedTextBox.SelectOnLoadChar = "";
+
+ }
+ (selectOnLoad /* || !rtfField?.Text*/) && this._editorView!.focus();
+ // add user mark for any first character that was typed since the user mark that gets set in KeyPress won't have been called yet.
+ this._editorView!.state.storedMarks = [...(this._editorView!.state.storedMarks ? this._editorView!.state.storedMarks : []), schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })];
+ }
+ getFont(font: string) {
+ switch (font) {
+ case "Arial": return schema.marks.arial.create();
+ case "Times New Roman": return schema.marks.timesNewRoman.create();
+ case "Georgia": return schema.marks.georgia.create();
+ case "Comic Sans MS": return schema.marks.comicSans.create();
+ case "Tahoma": return schema.marks.tahoma.create();
+ case "Impact": return schema.marks.impact.create();
+ case "ACrimson Textrial": return schema.marks.crimson.create();
+ }
+ return schema.marks.arial.create();
+ }
+
+ componentWillUnmount() {
+ Object.values(this._disposers).forEach(disposer => disposer?.());
+ this._editorView?.destroy();
+ }
+
+ static _downEvent: any;
+ _downX = 0;
+ _downY = 0;
+ _break = false;
+ onPointerDown = (e: React.PointerEvent): void => {
+ if (this._recording && !e.ctrlKey && e.button === 0) {
+ this.stopDictation(true);
+ this._break = true;
+ const state = this._editorView!.state;
+ const to = state.selection.to;
+ const updated = TextSelection.create(state.doc, to, to);
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(updated).insertText("\n", to));
+ e.preventDefault();
+ e.stopPropagation();
+ if (this._recording) setTimeout(() => this.recordDictation(), 500);
+ }
+ this._downX = e.clientX;
+ this._downY = e.clientY;
+ this.doLinkOnDeselect();
+ FormattedTextBox._downEvent = true;
+ FormattedTextBoxComment.textBox = this;
+ if (this.props.onClick && e.button === 0 && !this.props.isSelected(false)) {
+ e.preventDefault();
+ }
+ if (e.button === 0 && this.active(true) && !e.altKey && !e.ctrlKey && !e.metaKey) {
+ if (e.clientX < this.ProseRef!.getBoundingClientRect().right) { // don't stop propagation if clicking in the sidebar
+ e.stopPropagation();
+ }
+ }
+ if (e.button === 2 || (e.button === 0 && e.ctrlKey)) {
+ e.preventDefault();
+ }
+ }
+
+ onPointerUp = (e: React.PointerEvent): void => {
+ if (!FormattedTextBox._downEvent) return;
+ FormattedTextBox._downEvent = false;
+ if (!(e.nativeEvent as any).formattedHandled) {
+ FormattedTextBoxComment.textBox = this;
+ FormattedTextBoxComment.update(this._editorView!);
+ }
+ (e.nativeEvent as any).formattedHandled = true;
+
+ if (e.buttons === 1 && this.props.isSelected(true) && !e.altKey) {
+ e.stopPropagation();
+ }
+ this._downX = this._downY = Number.NaN;
+ }
+
+ @action
+ onFocused = (e: React.FocusEvent): void => {
+ FormattedTextBox.FocusedBox = this;
+ this.tryUpdateHeight();
+
+ // see if we need to preserve the insertion point
+ const prosediv = this.ProseRef?.children?.[0] as any;
+ const keeplocation = prosediv?.keeplocation;
+ prosediv && (prosediv.keeplocation = undefined);
+ const pos = this._editorView?.state.selection.$from.pos || 1;
+ keeplocation && setTimeout(() => this._editorView?.dispatch(this._editorView?.state.tr.setSelection(TextSelection.create(this._editorView.state.doc, pos))));
+ const coords = !Number.isNaN(this._downX) ? { left: this._downX, top: this._downY, bottom: this._downY, right: this._downX } : this._editorView?.coordsAtPos(pos);
+
+ // jump rich text menu to this textbox
+ const bounds = this._ref.current?.getBoundingClientRect();
+ if (bounds && this.props.Document._chromeStatus !== "disabled") {
+ const x = Math.min(Math.max(bounds.left, 0), window.innerWidth - RichTextMenu.Instance.width);
+ let y = Math.min(Math.max(0, bounds.top - RichTextMenu.Instance.height - 50), window.innerHeight - RichTextMenu.Instance.height);
+ if (coords && coords.left > x && coords.left < x + RichTextMenu.Instance.width && coords.top > y && coords.top < y + RichTextMenu.Instance.height + 50) {
+ y = Math.min(bounds.bottom, window.innerHeight - RichTextMenu.Instance.height);
+ }
+ RichTextMenu.Instance.jumpTo(x, y);
+ }
+ }
+ onPointerWheel = (e: React.WheelEvent): void => {
+ // if a text note is not selected and scrollable, this prevents us from being able to scroll and zoom out at the same time
+ if (this.props.isSelected(true) || e.currentTarget.scrollHeight > e.currentTarget.clientHeight) {
+ e.stopPropagation();
+ }
+ }
+
+ static _bulletStyleSheet: any = addStyleSheet();
+ static _userStyleSheet: any = addStyleSheet();
+
+ onClick = (e: React.MouseEvent): void => {
+ if ((this._editorView!.root as any).getSelection().isCollapsed) { // this is a hack to allow the cursor to be placed at the end of a document when the document ends in an inline dash comment. Apparently Chrome on Windows has a bug/feature which breaks this when clicking after the end of the text.
+ const pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ const node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos); // get what prosemirror thinks the clicked node is (if it's null, then we didn't click on any text)
+ if (pcords && node?.type === this._editorView!.state.schema.nodes.dashComment) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pcords.pos + 2)));
+ e.preventDefault();
+ }
+ if (!node && this.ProseRef) {
+ const lastNode = this.ProseRef.children[this.ProseRef.children.length - 1].children[this.ProseRef.children[this.ProseRef.children.length - 1].children.length - 1]; // get the last prosemirror div
+ if (e.clientY > lastNode?.getBoundingClientRect().bottom) { // if we clicked below the last prosemirror div, then set the selection to be the end of the document
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, this._editorView!.state.doc.content.size)));
+ }
+ }
+ }
+ if ((e.nativeEvent as any).formattedHandled) { e.stopPropagation(); return; }
+ (e.nativeEvent as any).formattedHandled = true;
+ // if (e.button === 0 && ((!this.props.isSelected(true) && !e.ctrlKey) || (this.props.isSelected(true) && e.ctrlKey)) && !e.metaKey && e.target) {
+ // let href = (e.target as any).href;
+ // let location: string;
+ // if ((e.target as any).attributes.location) {
+ // location = (e.target as any).attributes.location.value;
+ // }
+ // let pcords = this._editorView!.posAtCoords({ left: e.clientX, top: e.clientY });
+ // let node = pcords && this._editorView!.state.doc.nodeAt(pcords.pos);
+ // if (node) {
+ // let link = node.marks.find(m => m.type === this._editorView!.state.schema.marks.link);
+ // if (link && !(link.attrs.docref && link.attrs.title)) { // bcz: getting hacky. this indicates that we clicked on a PDF excerpt quotation. In this case, we don't want to follow the link (we follow only the actual hyperlink for the quotation which is handled above).
+ // href = link && link.attrs.href;
+ // location = link && link.attrs.location;
+ // }
+ // }
+ // if (href) {
+ // if (href.indexOf(Utils.prepend("/doc/")) === 0) {
+ // let linkClicked = href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ // if (linkClicked) {
+ // DocServer.GetRefField(linkClicked).then(async linkDoc => {
+ // (linkDoc instanceof Doc) &&
+ // DocumentManager.Instance.FollowLink(linkDoc, this.props.Document, document => this.props.addDocTab(document, location ? location : "inTab"), false);
+ // });
+ // }
+ // } else {
+ // let webDoc = Docs.Create.WebDocument(href, { x: NumCast(this.layoutDoc.x, 0) + NumCast(this.layoutDoc.width, 0), y: NumCast(this.layoutDoc.y) });
+ // this.props.addDocument && this.props.addDocument(webDoc);
+ // }
+ // e.stopPropagation();
+ // e.preventDefault();
+ // }
+ // }
+
+ if (Math.abs(e.clientX - this._downX) < 4 && Math.abs(e.clientX - this._downX) < 4) {
+ this.props.select(e.ctrlKey);
+ this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, false);
+ }
+ }
+
+ // this hackiness handles clicking on the list item bullets to do expand/collapse. the bullets are ::before pseudo elements so there's no real way to hit test against them.
+ hitBulletTargets(x: number, y: number, select: boolean, highlightOnly: boolean) {
+ clearStyleSheetRules(FormattedTextBox._bulletStyleSheet);
+ const pos = this._editorView!.posAtCoords({ left: x, top: y });
+ if (pos && this.props.isSelected(true)) {
+ // let beforeEle = document.querySelector("." + hit.className) as Element; // const before = hit ? window.getComputedStyle(hit, ':before') : undefined;
+ //const node = this._editorView!.state.doc.nodeAt(pos.pos);
+ const $pos = this._editorView!.state.doc.resolve(pos.pos);
+ let list_node = $pos.node().type === schema.nodes.list_item ? $pos.node() : undefined;
+ if ($pos.node().type === schema.nodes.ordered_list) {
+ for (let off = 1; off < 100; off++) {
+ const pos = this._editorView!.posAtCoords({ left: x + off, top: y });
+ const node = pos && this._editorView!.state.doc.nodeAt(pos.pos);
+ if (node?.type === schema.nodes.list_item) {
+ list_node = node;
+ break;
+ }
+ }
+ }
+ if (list_node && pos.inside >= 0 && this._editorView!.state.doc.nodeAt(pos.inside)!.attrs.bulletStyle === list_node.attrs.bulletStyle) {
+ if (select) {
+ const $olist_pos = this._editorView!.state.doc.resolve($pos.pos - $pos.parentOffset - 1);
+ if (!highlightOnly) {
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(new NodeSelection($olist_pos)));
+ }
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
+ } else if (Math.abs(pos.pos - pos.inside) < 2) {
+ if (!highlightOnly) {
+ const offset = this._editorView!.state.doc.nodeAt(pos.inside)?.type === schema.nodes.ordered_list ? 1 : 0;
+ this._editorView!.dispatch(this._editorView!.state.tr.setNodeMarkup(pos.inside + offset, list_node.type, { ...list_node.attrs, visibility: !list_node.attrs.visibility }));
+ this._editorView!.dispatch(this._editorView!.state.tr.setSelection(TextSelection.create(this._editorView!.state.doc, pos.inside + offset)));
+ }
+ addStyleSheetRule(FormattedTextBox._bulletStyleSheet, list_node.attrs.mapStyle + list_node.attrs.bulletStyle + ":hover:before", { background: "lightgray" });
+ }
+ }
+ }
+ }
+ onMouseUp = (e: React.MouseEvent): void => {
+ e.stopPropagation();
+
+ const view = this._editorView as any;
+ // this interposes on prosemirror's upHandler to prevent prosemirror's up from invoked multiple times when there
+ // are nested prosemirrors. We only want the lowest level prosemirror to be invoked.
+ if (view.mouseDown) {
+ const originalUpHandler = view.mouseDown.up;
+ view.root.removeEventListener("mouseup", originalUpHandler);
+ view.mouseDown.up = (e: MouseEvent) => {
+ !(e as any).formattedHandled && originalUpHandler(e);
+ // e.stopPropagation();
+ (e as any).formattedHandled = true;
+ };
+ view.root.addEventListener("mouseup", view.mouseDown.up);
+ }
+ }
+
+ richTextMenuPlugin() {
+ return new Plugin({
+ view(newView) {
+ RichTextMenu.Instance && RichTextMenu.Instance.changeView(newView);
+ return RichTextMenu.Instance;
+ }
+ });
+ }
+
+ public static HadSelection: boolean = false;
+ onBlur = (e: any) => {
+ FormattedTextBox.HadSelection = window.getSelection()?.toString() !== "";
+ //DictationManager.Controls.stop(false);
+ if (this._undoTyping) {
+ this._undoTyping.end();
+ this._undoTyping = undefined;
+ }
+ this.doLinkOnDeselect();
+
+ // move the richtextmenu offscreen
+ if (!RichTextMenu.Instance.Pinned && !RichTextMenu.Instance.overMenu) RichTextMenu.Instance.jumpTo(-300, -300);
+ }
+
+ _lastTimedMark: Mark | undefined = undefined;
+ onKeyPress = (e: React.KeyboardEvent) => {
+ if (e.altKey) {
+ e.preventDefault();
+ return;
+ }
+ const state = this._editorView!.state;
+ if (!state.selection.empty && e.key === "%") {
+ this._rules!.EnteringStyle = true;
+ e.preventDefault();
+ e.stopPropagation();
+ return;
+ }
+
+ if (state.selection.empty || !this._rules!.EnteringStyle) {
+ this._rules!.EnteringStyle = false;
+ }
+ if (e.key === "Escape") {
+ this._editorView!.dispatch(state.tr.setSelection(TextSelection.create(state.doc, state.selection.from, state.selection.from)));
+ (document.activeElement as any).blur?.();
+ SelectionManager.DeselectAll();
+ }
+ e.stopPropagation();
+ if (e.key === "Tab" || e.key === "Enter") {
+ e.preventDefault();
+ }
+ const mark = e.key !== " " && this._lastTimedMark ? this._lastTimedMark : schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) });
+ this._lastTimedMark = mark;
+ this._editorView!.dispatch(this._editorView!.state.tr.removeStoredMark(schema.marks.user_mark.create({})).addStoredMark(mark));
+
+ if (!this._undoTyping) {
+ this._undoTyping = UndoManager.StartBatch("undoTyping");
+ }
+ }
+
+ onscrolled = (ev: React.UIEvent) => {
+ this.props.Document.scrollPos = this._scrollRef.current!.scrollTop;
+ }
+ @action
+ tryUpdateHeight(limitHeight?: number) {
+ let scrollHeight = this._ref.current?.scrollHeight;
+ if (this.layoutDoc._autoHeight && scrollHeight &&
+ getComputedStyle(this._ref.current!.parentElement!).top === "0px") { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation
+ if (limitHeight && scrollHeight > limitHeight) {
+ scrollHeight = limitHeight;
+ this.layoutDoc.limitHeight = undefined;
+ this.layoutDoc._autoHeight = false;
+ }
+ const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.dataDoc._nativeHeight, 0);
+ const dh = NumCast(this.layoutDoc._height, 0);
+ const newHeight = Math.max(10, (nh ? dh / nh * scrollHeight : scrollHeight) + (this.props.ChromeHeight ? this.props.ChromeHeight() : 0));
+ if (Math.abs(newHeight - dh) > 1) { // bcz: Argh! without this, we get into a React crash if the same document is opened in a freeform view and in the treeview. no idea why, but after dragging the freeform document, selecting it, and selecting text, it will compute to 1 pixel higher than the treeview which causes a cycle
+ this.layoutDoc._height = newHeight;
+ this.dataDoc._nativeHeight = nh ? scrollHeight : undefined;
+ }
+ }
+ }
+
+ @computed get sidebarWidthPercent() { return StrCast(this.props.Document.sidebarWidthPercent, "0%"); }
+ sidebarWidth = () => Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100 * this.props.PanelWidth();
+ sidebarScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-(this.props.PanelWidth() - this.sidebarWidth()), 0);
+ @computed get sidebarColor() { return StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "transparent")); }
+ render() {
+ TraceMobx();
+ const rounded = StrCast(this.layoutDoc.borderRounding) === "100%" ? "-rounded" : "";
+ const interactive = InkingControl.Instance.selectedTool || this.layoutDoc.isBackground;
+ if (this.props.isSelected()) {
+ this._editorView && RichTextMenu.Instance.updateFromDash(this._editorView, undefined, this.props);
+ } else if (FormattedTextBoxComment.textBox === this) {
+ FormattedTextBoxComment.Hide();
+ }
+ return (
+
+ this.hitBulletTargets(e.clientX, e.clientY, e.shiftKey, true)}
+ onBlur={this.onBlur}
+ onPointerUp={this.onPointerUp}
+ onPointerDown={this.onPointerDown}
+ onMouseUp={this.onMouseUp}
+ onWheel={this.onPointerWheel}
+ onPointerEnter={action(() => this._entered = true)}
+ onPointerLeave={action((e: React.PointerEvent
) => {
+ 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;
+ }
+ }
+ })}
+ >
+
+ {!this.props.Document._showSidebar ? (null) : this.sidebarWidthPercent === "0%" ?
+ this.toggleSidebar()} /> :
+
+
+
+
this.toggleSidebar()} />
+
}
+ {!this.props.Document._showAudio ? (null) :
+
{
+ runInAction(() => this._recording = !this._recording);
+ setTimeout(() => this._editorView!.focus(), 500);
+ e.stopPropagation();
+ }} >
+
+
}
+
+ );
+ }
+}
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
new file mode 100644
index 000000000..2dd63ec21
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.scss
@@ -0,0 +1,33 @@
+.FormattedTextBox-tooltip {
+ position: absolute;
+ pointer-events: none;
+ z-index: 20;
+ background: white;
+ border: 1px solid silver;
+ border-radius: 2px;
+ margin-bottom: 7px;
+ -webkit-transform: translateX(-50%);
+ transform: translateX(-50%);
+ }
+ .FormattedTextBox-tooltip:before {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -6px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: silver;
+ }
+ .FormattedTextBox-tooltip:after {
+ content: "";
+ height: 0; width: 0;
+ position: absolute;
+ left: 50%;
+ margin-left: -5px;
+ bottom: -4.5px;
+ border: 5px solid transparent;
+ border-bottom-width: 0;
+ border-top-color: white;
+ }
\ No newline at end of file
diff --git a/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
new file mode 100644
index 000000000..f9e4c5210
--- /dev/null
+++ b/src/client/views/nodes/formattedText/FormattedTextBoxComment.tsx
@@ -0,0 +1,236 @@
+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 "./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 wiki from "wikijs";
+import { DocumentType } from "../../../documents/DocumentTypes";
+
+export let formattedTextBoxCommentPlugin = new Plugin({
+ view(editorView) { return new FormattedTextBoxComment(editorView); }
+});
+export function findOtherUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid && m.attrs.userid !== Doc.CurrentUserEmail);
+}
+export function findUserMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.attrs.userid);
+}
+export function findLinkMark(marks: Mark[]): Mark | undefined {
+ return marks.find(m => m.type === schema.marks.link);
+}
+export function findStartOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let before = 0;
+ let nbef = rpos.nodeBefore;
+ while (nbef && finder(nbef.marks)) {
+ before += nbef.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos - nbef.nodeSize);
+ rpos && (nbef = rpos.nodeBefore);
+ }
+ return before;
+}
+export function findEndOfMark(rpos: ResolvedPos, view: EditorView, finder: (marks: Mark[]) => Mark | undefined) {
+ let after = 0;
+ let naft = rpos.nodeAfter;
+ while (naft && finder(naft.marks)) {
+ after += naft.nodeSize;
+ rpos = view.state.doc.resolve(rpos.pos + naft.nodeSize);
+ rpos && (naft = rpos.nodeAfter);
+ }
+ return after;
+}
+
+
+export class FormattedTextBoxComment {
+ static tooltip: HTMLElement;
+ static tooltipText: HTMLElement;
+ static tooltipInput: HTMLInputElement;
+ static start: number;
+ static end: number;
+ static mark: Mark;
+ static textBox: FormattedTextBox | undefined;
+ static linkDoc: Doc | undefined;
+ constructor(view: any) {
+ if (!FormattedTextBoxComment.tooltip) {
+ const root = document.getElementById("root");
+ FormattedTextBoxComment.tooltipInput = document.createElement("input");
+ FormattedTextBoxComment.tooltipInput.type = "checkbox";
+ FormattedTextBoxComment.tooltip = document.createElement("div");
+ FormattedTextBoxComment.tooltipText = document.createElement("div");
+ FormattedTextBoxComment.tooltipText.style.width = "100%";
+ FormattedTextBoxComment.tooltipText.style.height = "100%";
+ FormattedTextBoxComment.tooltipText.style.textOverflow = "ellipsis";
+ FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipText);
+ FormattedTextBoxComment.tooltip.className = "FormattedTextBox-tooltip";
+ FormattedTextBoxComment.tooltip.style.pointerEvents = "all";
+ FormattedTextBoxComment.tooltip.style.maxWidth = "350px";
+ FormattedTextBoxComment.tooltip.style.maxHeight = "250px";
+ FormattedTextBoxComment.tooltip.style.width = "100%";
+ FormattedTextBoxComment.tooltip.style.height = "100%";
+ FormattedTextBoxComment.tooltip.style.overflow = "hidden";
+ FormattedTextBoxComment.tooltip.style.display = "none";
+ FormattedTextBoxComment.tooltip.appendChild(FormattedTextBoxComment.tooltipInput);
+ FormattedTextBoxComment.tooltip.onpointerdown = (e: PointerEvent) => {
+ const keep = e.target && (e.target as any).type === "checkbox" ? true : false;
+ const textBox = FormattedTextBoxComment.textBox;
+ if (FormattedTextBoxComment.linkDoc && !keep && textBox) {
+ if (FormattedTextBoxComment.linkDoc.type !== DocumentType.LINK) {
+ textBox.props.addDocTab(FormattedTextBoxComment.linkDoc, e.ctrlKey ? "inTab" : "onRight");
+ } else {
+ DocumentManager.Instance.FollowLink(FormattedTextBoxComment.linkDoc, textBox.props.Document,
+ (doc: Doc, followLinkLocation: string) => textBox.props.addDocTab(doc, e.ctrlKey ? "inTab" : followLinkLocation));
+ }
+ } else if (textBox && (FormattedTextBoxComment.tooltipText as any).href) {
+ textBox.props.addDocTab(Docs.Create.WebDocument((FormattedTextBoxComment.tooltipText as any).href, { title: (FormattedTextBoxComment.tooltipText as any).href, _width: 200, _height: 400 }), "onRight");
+ }
+ keep && textBox && FormattedTextBoxComment.start !== undefined && textBox.adoptAnnotation(
+ FormattedTextBoxComment.start, FormattedTextBoxComment.end, FormattedTextBoxComment.mark);
+ e.stopPropagation();
+ e.preventDefault();
+ };
+ root && root.appendChild(FormattedTextBoxComment.tooltip);
+ }
+ }
+
+ public static Hide() {
+ FormattedTextBoxComment.textBox = undefined;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "none");
+ ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
+ }
+ public static SetState(textBox: any, start: number, end: number, mark: Mark) {
+ FormattedTextBoxComment.textBox = textBox;
+ FormattedTextBoxComment.start = start;
+ FormattedTextBoxComment.end = end;
+ FormattedTextBoxComment.mark = mark;
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = "");
+ }
+
+ static update(view: EditorView, lastState?: EditorState) {
+ const state = view.state;
+ // Don't do anything if the document/selection didn't change
+ if (lastState && lastState.doc.eq(state.doc) &&
+ lastState.selection.eq(state.selection)) {
+ return;
+ }
+ FormattedTextBoxComment.linkDoc = undefined;
+
+ const textBox = FormattedTextBoxComment.textBox;
+ if (!textBox || !textBox.props) {
+ return;
+ }
+ let set = "none";
+ let nbef = 0;
+ FormattedTextBoxComment.tooltipInput.style.display = "none";
+ FormattedTextBoxComment.tooltip.style.width = "";
+ FormattedTextBoxComment.tooltip.style.height = "";
+ (FormattedTextBoxComment.tooltipText as any).href = "";
+ FormattedTextBoxComment.tooltipText.style.whiteSpace = "";
+ FormattedTextBoxComment.tooltipText.style.overflow = "";
+ // this section checks to see if the insertion point is over text entered by a different user. If so, it sets ths comment text to indicate the user and the modification date
+ if (state.selection.$from) {
+ nbef = findStartOfMark(state.selection.$from, view, findOtherUserMark);
+ const naft = findEndOfMark(state.selection.$from, view, findOtherUserMark);
+ const noselection = view.state.selection.$from === view.state.selection.$to;
+ let child: any = null;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
+ const mark = child && findOtherUserMark(child.marks);
+ if (mark && child && (nbef || naft) && (!mark.attrs.opened || noselection)) {
+ FormattedTextBoxComment.SetState(FormattedTextBoxComment.textBox, state.selection.$from.pos - nbef, state.selection.$from.pos + naft, mark);
+ }
+ if (mark && child && ((nbef && naft) || !noselection)) {
+ FormattedTextBoxComment.tooltipText.textContent = mark.attrs.userid + " date=" + (new Date(mark.attrs.modified * 5000)).toDateString();
+ set = "";
+ FormattedTextBoxComment.tooltipInput.style.display = "";
+ }
+ }
+ // this checks if the selection is a hyperlink. If so, it displays the target doc's text for internal links, and the url of the target for external links.
+ if (set === "none" && state.selection.$from) {
+ nbef = findStartOfMark(state.selection.$from, view, findLinkMark);
+ const naft = findEndOfMark(state.selection.$from, view, findLinkMark);
+ let child: any = null;
+ state.doc.nodesBetween(state.selection.from, state.selection.to, (node: any, pos: number, parent: any) => !child && node.marks.length && (child = node));
+ const mark = child && findLinkMark(child.marks);
+ if (mark && child && nbef && naft && mark.attrs.showPreview) {
+ FormattedTextBoxComment.tooltipText.textContent = "external => " + mark.attrs.href;
+ (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
+ if (mark.attrs.href.startsWith("https://en.wikipedia.org/wiki/")) {
+ wiki().page(mark.attrs.href.replace("https://en.wikipedia.org/wiki/", "")).then(page => page.summary().then(summary => FormattedTextBoxComment.tooltipText.textContent = summary.substring(0, 500)));
+ } else {
+ FormattedTextBoxComment.tooltipText.style.whiteSpace = "pre";
+ FormattedTextBoxComment.tooltipText.style.overflow = "hidden";
+ }
+ if (mark.attrs.href.indexOf(Utils.prepend("/doc/")) === 0) {
+ FormattedTextBoxComment.tooltipText.textContent = "target not found...";
+ (FormattedTextBoxComment.tooltipText as any).href = "";
+ const docTarget = mark.attrs.href.replace(Utils.prepend("/doc/"), "").split("?")[0];
+ try {
+ ReactDOM.unmountComponentAtNode(FormattedTextBoxComment.tooltipText);
+ } catch (e) { }
+ docTarget && DocServer.GetRefField(docTarget).then(async linkDoc => {
+ if (linkDoc instanceof Doc) {
+ (FormattedTextBoxComment.tooltipText as any).href = mark.attrs.href;
+ FormattedTextBoxComment.linkDoc = linkDoc;
+ const anchor = FieldValue(Doc.AreProtosEqual(FieldValue(Cast(linkDoc.anchor1, Doc)), textBox.dataDoc) ? Cast(linkDoc.anchor2, Doc) : (Cast(linkDoc.anchor1, Doc)) || linkDoc);
+ const target = anchor?.annotationOn ? await DocCastAsync(anchor.annotationOn) : anchor;
+ if (anchor !== target && anchor && target) {
+ target.scrollY = NumCast(anchor?.y);
+ }
+ if (target) {
+ ReactDOM.render(
Math.min(350, NumCast(target._width, 350))}
+ PanelHeight={() => Math.min(250, NumCast(target._height, 250))}
+ focus={emptyFunction}
+ whenActiveChanged={returnFalse}
+ />, FormattedTextBoxComment.tooltipText);
+ FormattedTextBoxComment.tooltip.style.width = NumCast(target.width) ? `${NumCast(target.width)}` : "100%";
+ FormattedTextBoxComment.tooltip.style.height = NumCast(target.height) ? `${NumCast(target.height)}` : "100%";
+ }
+ // let ext = (target && target.type !== DocumentType.PDFANNO && Doc.fieldExtensionDoc(target, "data")) || target; // try guessing that the target doc's data is in the 'data' field. probably need an 'overviewLayout' and then just display the target Document ....
+ // let text = ext && StrCast(ext.text);
+ // ext && (FormattedTextBoxComment.tooltipText.textContent = (target && target.type === DocumentType.PDFANNO ? "Quoted from " : "") + "=> " + (text || StrCast(ext.title)));
+ }
+ });
+ }
+ set = "";
+ }
+ }
+ if (set !== "none") {
+ // These are in screen coordinates
+ // let start = view.coordsAtPos(state.selection.from), end = view.coordsAtPos(state.selection.to);
+ const start = view.coordsAtPos(state.selection.from - nbef), end = view.coordsAtPos(state.selection.from - nbef);
+ // The box in which the tooltip is positioned, to use as base
+ const box = (document.getElementsByClassName("mainView-container") as any)[0].getBoundingClientRect();
+ // Find a center-ish x position from the selection endpoints (when
+ // crossing lines, end may be more to the left)
+ const left = Math.max((start.left + end.left) / 2, start.left + 3);
+ FormattedTextBoxComment.tooltip.style.left = (left - box.left) + "px";
+ FormattedTextBoxComment.tooltip.style.bottom = (box.bottom - start.top) + "px";
+ }
+ FormattedTextBoxComment.tooltip && (FormattedTextBoxComment.tooltip.style.display = set);
+ }
+
+ destroy() { }
+}
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 {
+ 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 (
+
+

+
+
+
+
+
+ );
+ }
+}
\ 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,
+ 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 `` 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>(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, dispatch: (tx: Transaction) => 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, dispatch: (tx: Transaction) => 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, dispatch: (tx: Transaction) => void) => {
+ dispatch(state.tr.replaceSelectionWith(hr.create()).scrollIntoView());
+ return true;
+ });
+
+ bind("Tab", (state: EditorState, dispatch: (tx: Transaction) => 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, dispatch: (tx: Transaction) => 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, dispatch: (tx: Transaction) => 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, dispatch: (tx: Transaction>) => void) => {
+ return addTextOnRight(true);
+ });
+ bind("Enter", (state: EditorState, dispatch: (tx: Transaction>) => 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>) => void))) {
+ dispatch(tx3);
+ }
+ })) {
+ return false;
+ }
+ }
+ return true;
+ });
+ bind("Space", (state: EditorState, dispatch: (tx: Transaction) => void) => {
+ const marks = state.storedMarks || (state.selection.$to.parentOffset && state.selection.$from.marks());
+ dispatch(splitMetadata(marks, state.tr));
+ return false;
+ });
+ bind(":", (state: EditorState, dispatch: (tx: Transaction) => 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> = 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 = 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, 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();
+ styles.set("families", activeFamilies);
+ styles.set("sizes", activeSizes);
+ return styles;
+ }
+
+ getMarksInSelection(state: EditorState) {
+ const found = new Set();
+ 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 (
+
+ );
+ }
+
+ 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 ?
+ :
+ ;
+ }
+ return label === activeOption ?
+ :
+ ;
+ });
+
+ const self = this;
+ function onChange(e: React.ChangeEvent) {
+ e.stopPropagation();
+ e.preventDefault();
+ options.forEach(({ label, mark, command }) => {
+ if (e.target.value === label) {
+ self.view && mark && command(mark, self.view);
+ }
+ });
+ }
+ return ;
+ }
+
+ 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 ?
+ :
+ ;
+ }
+ return label === activeOption ?
+ :
+ ;
+ });
+
+ const self = this;
+ function onChange(val: string) {
+ options.forEach(({ label, node, command }) => {
+ if (val === label) {
+ self.view && node && command(node);
+ }
+ });
+ }
+ return ;
+ }
+
+ 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, 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();
+
+ 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 =
+ ;
+
+ const dropdownContent =
+
+
{label}
+
+
+
;
+
+ return (
+
+ );
+ }
+
+ @action
+ clearBrush() {
+ RichTextMenu.Instance.brushIsEmpty = true;
+ RichTextMenu.Instance.brushMarks = new Set();
+ }
+
+ @action
+ fillBrush(state: EditorState, 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 =
+ ;
+
+ const dropdownContent =
+
+
Change font color:
+
+ {this.fontColors.map(color => {
+ if (color) {
+ return this.activeFontColor === color ?
+ :
+ ;
+ }
+ })}
+
+
;
+
+ return (
+
+ );
+ }
+
+ public insertColor(color: String, state: EditorState, 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 =
+ ;
+
+ const dropdownContent =
+
+
Change highlight color:
+
+ {this.highlightColors.map(color => {
+ if (color) {
+ return this.activeHighlightColor === color ?
+ :
+ ;
+ }
+ })}
+
+
;
+
+ return (
+
+ );
+ }
+
+ insertHighlight(color: String, state: EditorState, 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) {
+ self.setCurrentLink(e.target.value);
+ }
+
+ const link = this.currentLink ? this.currentLink : "";
+
+ const button = ;
+
+ const dropdownContent =
+
+
Linked to:
+
+
+
+
+
;
+
+ return (
+
+ );
+ }
+
+ 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): 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 = {[
+ 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),
+ ]}
;
+
+ const row2 =
+
+ {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size"),
+ this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family"),
+ this.createNodesDropdown(this.activeListType, this.listTypeOptions, "nodes")]}
+
+
+
+
+
+
+ {this.getDragger()}
+
+
;
+
+ return (
+
+ {this.getElementWithRows([row1, row2], 2, false)}
+
+ );
+ }
+}
+
+interface ButtonDropdownProps {
+ view?: EditorView;
+ button: JSX.Element;
+ dropdownContent: JSX.Element;
+ openDropdownOnButton?: boolean;
+}
+
+@observer
+class ButtonDropdown extends React.Component {
+
+ @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 (
+ this.ref = node}>
+ {this.props.openDropdownOnButton ?
+ :
+ <>
+ {this.props.button}
+
+ >}
+
+ {this.showDropdown ? this.props.dropdownContent : (null)}
+
+ );
+ }
+}
\ 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 #
+ 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 [[ : ]] // [[: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 {{ : }} // {{:Doc}} => show default view of document {{}} => show layout for this doc {{ : 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(, 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();
+ }
+ 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 {
+
+ 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 (
+
+
+
+ );
+
+ }
+}
\ 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 ``
+ // 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 `` element.
+ // Has parse rules that also match `` and `font-style: italic`.
+ em: {
+ parseDOM: [{ tag: "i" }, { tag: "em" }, { style: "font-style: italic" }],
+ toDOM() { return emDOM; }
+ },
+
+ // :: MarkSpec A strong mark. Rendered as ``, parse rules
+ // also match `` 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 `` 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 (``) wrapping one or more blocks.
+ blockquote: {
+ content: "block+",
+ group: "block",
+ defining: true,
+ parseDOM: [{ tag: "blockquote" }],
+ toDOM() { return blockquoteDOM; }
+ },
+
+ // :: NodeSpec A horizontal rule (`
`).
+ 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 `` to
+ // `` 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 `` element with a
+ // `` 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 (`
`) 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 `
`.
+ 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) {
+ 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) {
+ 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
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 73ebbb303..69a80e1b4 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -7,7 +7,7 @@ import { observer } from 'mobx-react';
import { DocServer } from '../client/DocServer';
import { Docs } from '../client/documents/Documents';
import { DocumentManager } from '../client/util/DocumentManager';
-import RichTextMenu from '../client/util/RichTextMenu';
+import RichTextMenu from '../client/views/nodes/formattedText/RichTextMenu';
import { Scripting } from '../client/util/Scripting';
import { Transform } from '../client/util/Transform';
import { CollectionView } from '../client/views/collections/CollectionView';
diff --git a/src/new_fields/RichTextUtils.ts b/src/new_fields/RichTextUtils.ts
index c9960e783..c475d0d73 100644
--- a/src/new_fields/RichTextUtils.ts
+++ b/src/new_fields/RichTextUtils.ts
@@ -4,11 +4,11 @@ import { Fragment, Mark, Node } from "prosemirror-model";
import { sinkListItem } from "prosemirror-schema-list";
import { Utils } from "../Utils";
import { Docs } from "../client/documents/Documents";
-import { schema } from "../client/util/schema_rts";
+import { schema } from "../client/views/nodes/formattedText/schema_rts";
import { GooglePhotos } from "../client/apis/google_docs/GooglePhotosClientUtils";
import { DocServer } from "../client/DocServer";
import { Networking } from "../client/Network";
-import { FormattedTextBox } from "../client/views/nodes/FormattedTextBox";
+import { FormattedTextBox } from "../client/views/nodes/formattedText/FormattedTextBox";
import { Doc, Opt } from "./Doc";
import { Id } from "./FieldSymbols";
import { RichTextField } from "./RichTextField";
diff --git a/src/server/authentication/models/current_user_utils.ts b/src/server/authentication/models/current_user_utils.ts
index 1d41c3570..08dc21460 100644
--- a/src/server/authentication/models/current_user_utils.ts
+++ b/src/server/authentication/models/current_user_utils.ts
@@ -17,7 +17,7 @@ import { CollectionViewType } from "../../../client/views/collections/Collection
import { makeTemplate } from "../../../client/util/DropConverter";
import { RichTextField } from "../../../new_fields/RichTextField";
import { PrefetchProxy } from "../../../new_fields/Proxy";
-import { FormattedTextBox } from "../../../client/views/nodes/FormattedTextBox";
+import { FormattedTextBox } from "../../../client/views/nodes/formattedText/FormattedTextBox";
import { MainView } from "../../../client/views/MainView";
import { DocumentType } from "../../../client/documents/DocumentTypes";
import { SchemaHeaderField } from "../../../new_fields/SchemaHeaderField";
--
cgit v1.2.3-70-g09d2