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 "../views/collections/CollectionView";
import { DocumentView } from "../views/nodes/DocumentView";
import { FormattedTextBox } from "../views/nodes/FormattedTextBox";
import { DocumentManager } from "./DocumentManager";
import { Transform } from "./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);
}
}