diff options
Diffstat (limited to 'src/client/util/RichTextSchema.tsx')
-rw-r--r-- | src/client/util/RichTextSchema.tsx | 584 |
1 files changed, 471 insertions, 113 deletions
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx index f567d803e..1109fd292 100644 --- a/src/client/util/RichTextSchema.tsx +++ b/src/client/util/RichTextSchema.tsx @@ -1,7 +1,24 @@ -import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model"; +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 { TextSelection } from "prosemirror-state"; +import { EditorState, TextSelection, NodeSelection } from "prosemirror-state"; +import { StepMap } from "prosemirror-transform"; +import { EditorView } from "prosemirror-view"; import { Doc } from "../../new_fields/Doc"; +import { FormattedTextBox } from "../views/nodes/FormattedTextBox"; +import { DocServer } from "../DocServer"; +import { DocumentManager } from "./DocumentManager"; +import ParagraphNodeSpec from "./ParagraphNodeSpec"; +import React = require("react"); +import { action, Lambda, observable, reaction, computed, runInAction, trace } from "mobx"; +import { observer } from "mobx-react"; +import * as ReactDOM from 'react-dom'; +import { DocumentView } from "../views/nodes/DocumentView"; +import { returnFalse, emptyFunction, returnEmptyString, returnOne } from "../../Utils"; +import { Transform } from "./Transform"; +import { NumCast } from "../../new_fields/Types"; const pDOM: DOMOutputSpecArray = ["p", 0], blockquoteDOM: DOMOutputSpecArray = ["blockquote", 0], hrDOM: DOMOutputSpecArray = ["hr"], preDOM: DOMOutputSpecArray = ["pre", ["code", 0]], brDOM: DOMOutputSpecArray = ["br"], ulDOM: DOMOutputSpecArray = ["ul", 0]; @@ -14,23 +31,32 @@ export const nodes: { [index: string]: NodeSpec } = { content: "block+" }, - // :: NodeSpec A plain paragraph textblock. Represented in the DOM - // as a `<p>` element. - paragraph: { + + footnote: { + group: "inline", content: "inline*", - group: "block", - parseDOM: [{ tag: "p" }], - toDOM() { return pDOM; } + 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" }] }, - // starmine: { - // inline: true, - // attrs: { oldtext: { default: "" } }, - // group: "inline", - // toDOM() { return ["star", "㊉"]; }, - // parseDOM: [{ tag: "star" }] + // // :: NodeSpec A plain paragraph textblock. Represented in the DOM + // // as a `<p>` element. + // paragraph: { + // content: "inline*", + // group: "block", + // parseDOM: [{ tag: "p" }], + // toDOM() { return pDOM; } // }, + paragraph: ParagraphNodeSpec, + // :: NodeSpec A blockquote (`<blockquote>`) wrapping one or more blocks. blockquote: { content: "block+", @@ -88,8 +114,6 @@ export const nodes: { [index: string]: NodeSpec } = { visibility: { default: false }, text: { default: undefined }, textslice: { default: undefined }, - textlen: { default: 0 } - }, group: "inline", toDOM(node) { @@ -113,9 +137,12 @@ export const nodes: { [index: string]: NodeSpec } = { inline: true, attrs: { src: {}, - width: { default: "100px" }, + width: { default: 100 }, alt: { default: null }, - title: { default: null } + title: { default: null }, + float: { default: "left" }, + location: { default: "onRight" }, + docid: { default: "" } }, group: "inline", draggable: true, @@ -129,13 +156,42 @@ export const nodes: { [index: string]: NodeSpec } = { }; } }], - // TODO if we don't define toDom, something weird happens: dragging the image will not move it but clone it. Why? + // 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: "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}, height: ${node.attrs.height}` }; + return ["div", { ...node.attrs, ...attrs }]; + } + }, + video: { inline: true, attrs: { @@ -176,47 +232,48 @@ export const nodes: { [index: string]: NodeSpec } = { content: 'list_item+', group: 'block', attrs: { - bulletStyle: { default: "" }, - mapStyle: { default: "decimal" } + bulletStyle: { default: 0 }, + mapStyle: { default: "decimal" }, + setFontSize: { default: undefined }, + inheritedFontSize: { default: undefined }, + visibility: { default: true } }, toDOM(node: Node<any>) { const bs = node.attrs.bulletStyle; - const decMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "decimal2" : bs === "indent3" ? "decimal3" : bs === "indent4" ? "decimal4" : ""; - const multiMap = bs === "indent1" ? "decimal" : bs === "indent2" ? "upper-alpha" : bs === "indent3" ? "lower-roman" : bs === "indent4" ? "lower-alpha" : ""; + const decMap = bs ? "decimal" + bs : ""; + const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; - for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = map; - return ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0]; - //return ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle};`, 0] + let fsize = node.attrs.setFontSize ? node.attrs.setFontSize : node.attrs.inheritedFontSize; + return node.attrs.visibility ? ['ol', { class: `${map}-ol`, style: `list-style: none;font-size: ${fsize}` }, 0] : + ['ol', { class: `${map}-ol`, style: `list-style: none; font-size: ${fsize}` }]; } }, - //this doesn't currently work for some reason + bullet_list: { ...bulletList, content: 'list_item+', group: 'block', // parseDOM: [{ tag: "ul" }, { style: 'list-style-type=disc' }], toDOM(node: Node<any>) { - for (let i = 0; i < node.childCount; i++) node.child(i).attrs.className = ""; return ['ul', 0]; } }, - //bullet_list: { - // content: 'list_item+', - // group: 'block', - //active: blockActive(schema.nodes.bullet_list), - //enable: wrapInList(schema.nodes.bullet_list), - //run: wrapInList(schema.nodes.bullet_list), - //select: state => true, - // }, list_item: { attrs: { - className: { default: "" } + bulletStyle: { default: 0 }, + mapStyle: { default: "decimal" }, + visibility: { default: true } }, ...listItem, content: 'paragraph block*', toDOM(node: any) { - return ["li", { class: node.attrs.className }, 0]; + const bs = node.attrs.bulletStyle; + const decMap = bs ? "decimal" + bs : ""; + const multiMap = bs === 1 ? "decimal1" : bs === 2 ? "upper-alpha" : bs === 3 ? "lower-roman" : bs === 4 ? "lower-alpha" : ""; + let map = node.attrs.mapStyle === "decimal" ? decMap : multiMap; + return node.attrs.visibility ? ["li", { class: `${map}` }, 0] : ["li", { class: `${map}` }, "..."]; + //return ["li", { class: `${map}` }, 0]; } }, }; @@ -224,7 +281,6 @@ export const nodes: { [index: string]: NodeSpec } = { const emDOM: DOMOutputSpecArray = ["em", 0]; const strongDOM: DOMOutputSpecArray = ["strong", 0]; const codeDOM: DOMOutputSpecArray = ["code", 0]; -const underlineDOM: DOMOutputSpecArray = ["underline", 0]; // :: Object [Specs](#model.MarkSpec) for the marks in the schema. export const marks: { [index: string]: MarkSpec } = { @@ -235,7 +291,8 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { href: {}, location: { default: null }, - title: { 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: [{ @@ -243,7 +300,11 @@ export const marks: { [index: string]: MarkSpec } = { return { href: dom.getAttribute("href"), location: dom.getAttribute("location"), title: dom.getAttribute("title") }; } }], - toDOM(node: any) { return ["a", node.attrs, 0]; } + toDOM(node: any) { + return node.attrs.docref && node.attrs.title ? + ["div", ["span", `"`], ["span", 0], ["span", `"`], ["br"], ["a", { ...node.attrs, class: "prosemirror-attribution" }, node.attrs.title], ["br"]] : + ["a", { ...node.attrs }, 0]; + } }, // :: MarkSpec An emphasis mark. Rendered as an `<em>` element. @@ -262,16 +323,6 @@ export const marks: { [index: string]: MarkSpec } = { toDOM() { return strongDOM; } }, - underline: { - parseDOM: [ - { tag: 'u' }, - { style: 'text-decoration=underline' } - ], - toDOM: () => ['span', { - style: 'text-decoration:underline' - }] - }, - strikethrough: { parseDOM: [ { tag: 'strike' }, @@ -312,15 +363,68 @@ export const marks: { [index: string]: MarkSpec } = { } }, + 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']; + } + }, + highlight: { - parseDOM: [{ style: 'color: blue' }], + parseDOM: [ + { + tag: "span", + getAttrs: (p: any) => { + if (typeof (p) !== "string") { + let 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; + } + }, + ], + inclusive: true, toDOM() { return ['span', { - style: 'color: blue' + 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") { + let 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: { parseDOM: [{ style: 'background: yellow' }], toDOM() { @@ -335,15 +439,18 @@ export const marks: { [index: string]: MarkSpec } = { attrs: { userid: { default: "" }, hide_users: { default: [] }, - opened: { default: false } + opened: { default: true }, + modified: { default: "when?" } }, group: "inline", - inclusive: false, toDOM(node: any) { let hideUsers = node.attrs.hide_users; let hidden = hideUsers.indexOf(node.attrs.userid) !== -1 || (hideUsers.length === 0 && node.attrs.userid !== Doc.CurrentUserEmail); return hidden ? - ['span', { class: node.attrs.opened ? "userMarkOpen" : "userMark" }, 0] : + (node.attrs.opened ? + ['span', { class: "userMarkOpen" }, 0] : + ['span', { class: "userMark" }, ['span', 0]] + ) : ['span', 0]; } }, @@ -355,6 +462,24 @@ export const marks: { [index: string]: MarkSpec } = { toDOM() { return codeDOM; } }, + // pFontFamily: { + // attrs: { + // style: { default: 'font-family: "Times New Roman", Times, serif;' }, + // }, + // parseDOM: [{ + // tag: "span", getAttrs(dom: any) { + // if (getComputedStyle(dom).font === "Times New Roman") return { style: `font-family: "Times New Roman", Times, serif;` }; + // if (getComputedStyle(dom).font === "Arial, Helvetica") return { style: `font-family: Arial, Helvetica, sans-serif;` }; + // if (getComputedStyle(dom).font === "Georgia") return { style: `font-family: Georgia, serif;` }; + // if (getComputedStyle(dom).font === "Comic Sans") return { style: `font-family: "Comic Sans MS", cursive, sans-serif;` }; + // if (getComputedStyle(dom).font === "Tahoma, Geneva") return { style: `font-family: Tahoma, Geneva, sans-serif;` }; + // } + // }], + // toDOM: (node: any) => ['span', { + // style: node.attrs.style + // }] + // }, + /* FONTS */ timesNewRoman: { @@ -499,22 +624,21 @@ export const marks: { [index: string]: MarkSpec } = { }] }, }; -function getFontSize(element: any) { - return parseFloat((getComputedStyle(element) as any).fontSize); -} export class ImageResizeView { _handle: HTMLElement; _img: HTMLElement; _outer: HTMLElement; - constructor(node: any, view: any, getPos: any) { + constructor(node: any, view: any, getPos: any, addDocTab: any) { 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; this._img.setAttribute("src", node.attrs.src); this._img.style.width = "100%"; @@ -527,32 +651,51 @@ export class ImageResizeView { this._handle.style.bottom = "-10px"; this._handle.style.right = "-10px"; let self = this; + 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)))); + } + }; + 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, undefined, node.attrs.location ? node.attrs.location : "inTab"), false)); + } + }; this._handle.onpointerdown = function (e: any) { e.preventDefault(); e.stopPropagation(); + let wid = Number(getComputedStyle(self._img).width!.replace(/px/, "")); + let 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); - view.dispatch( - view.state.tr.setNodeMarkup(getPos(), null, - { src: node.attrs.src, width: self._outer.style.width }) - .setSelection(view.state.selection)); + let 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); }; - this._outer.appendChild(this._handle); this._outer.appendChild(this._img); + this._outer.appendChild(this._handle); (this as any).dom = this._outer; } @@ -569,74 +712,292 @@ export class ImageResizeView { } } +export class DashDocView { + _handle: HTMLElement; + _dashSpan: HTMLDivElement; + _outer: HTMLElement; + constructor(node: any, view: any, getPos: any, addDocTab: any) { + this._handle = document.createElement("span"); + this._dashSpan = document.createElement("div"); + 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; + + 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._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"; + DocServer.GetRefField(node.attrs.docid).then(async dashDoc => { + if (dashDoc instanceof Doc) { + let scale = () => 100 / NumCast(dashDoc.nativeWidth, 100); + ReactDOM.render(<DocumentView + fitToBox={true} + Document={dashDoc} + addDocument={returnFalse} + removeDocument={returnFalse} + ruleProvider={undefined} + ScreenToLocalTransform={Transform.Identity} + addDocTab={returnFalse} + pinToPres={returnFalse} + renderDepth={1} + PanelWidth={() => 100} + PanelHeight={() => 100} + focus={emptyFunction} + backgroundColor={returnEmptyString} + parentActive={returnFalse} + whenActiveChanged={returnFalse} + bringToFront={emptyFunction} + zoomToScale={emptyFunction} + getScale={returnOne} + ContainingCollectionView={undefined} + ContainingCollectionDoc={undefined} + ContentScaling={scale} + ></DocumentView>, this._dashSpan); + } + }); + let self = this; + this._dashSpan.onclick = function (e: any) { + FormattedTextBox.firstTarget && FormattedTextBox.firstTarget(); + e.stopPropagation(); + }; + this._dashSpan.onkeydown = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeypress = function (e: any) { e.stopPropagation(); }; + this._dashSpan.onkeyup = function (e: any) { e.stopPropagation(); }; + this._handle.onpointerdown = function (e: any) { + e.preventDefault(); + e.stopPropagation(); + const startX = e.pageX; + const startY = e.pageY; + const startWidth = parseFloat(node.attrs.width); + const startHeight = parseFloat(node.attrs.height); + const onpointermove = (e: any) => { + const diffInPx = e.pageX - startX; + const diffInPy = e.pageY - startY; + self._outer.style.width = `${startWidth + diffInPx}`; + self._outer.style.height = `${startHeight + diffInPy}`; + }; + + const onpointerup = () => { + document.removeEventListener("pointermove", onpointermove); + document.removeEventListener("pointerup", onpointerup); + let 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); + }; + + this._outer.appendChild(this._dashSpan); + this._outer.appendChild(this._handle); + (this as any).dom = this._outer; + } + + selectNode() { + this._dashSpan.classList.add("ProseMirror-selectednode"); + + this._handle.style.display = ""; + } + + deselectNode() { + this._dashSpan.classList.remove("ProseMirror-selectednode"); + + this._handle.style.display = "none"; + } +} + +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 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 + let 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) + })] + }), + // 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) { + let { state, transactions } = this.innerView.state.applyTransaction(tr); + this.innerView.updateState(state); + + if (!tr.getMeta("fromOutside")) { + let outerTr = this.outerView.state.tr, offsetMap = StepMap.offset(this.getPos() + 1); + for (let transaction of transactions) { + let steps = transaction.steps; + for (let 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) { + let state = this.innerView.state; + let start = node.content.findDiffStart(state.doc.content); + if (start !== null) { + let { a: endA, b: endB } = node.content.findDiffEnd(state.doc.content); + let 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 SummarizedView { - // TODO: highlight text that is summarized. to find end of region, walk along mark _collapsed: HTMLElement; _view: any; constructor(node: any, view: any, getPos: any) { this._collapsed = document.createElement("span"); - this._collapsed.textContent = node.attrs.visibility ? "㊀" : "㊉"; - this._collapsed.style.opacity = "0.5"; - this._collapsed.style.position = "relative"; - this._collapsed.style.width = "40px"; - this._collapsed.style.height = "20px"; - let self = this; + 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 = function (e: any) { - if (node.attrs.visibility) { - // node.attrs.visibility = !node.attrs.visibility; - let y = getPos(); - const attrs = { ...node.attrs }; - attrs.visibility = !attrs.visibility; - let { from, to } = self.updateSummarizedText(y + 1, view.state.schema.marks.highlight); - let length = to - from; - let newSelection = TextSelection.create(view.state.doc, y + 1, y + 1 + length); - // update attrs of node - attrs.text = newSelection.content(); - attrs.textslice = newSelection.content().toJSON(); - view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); - view.dispatch(view.state.tr.setSelection(newSelection).deleteSelection(view.state, () => { })); - let marks = view.state.storedMarks.filter((m: any) => m.type !== view.state.schema.marks.highlight); - view.state.storedMarks = marks; - self._collapsed.textContent = "㊉"; - } else { - // node.attrs.visibility = !node.attrs.visibility; - let y = getPos(); - const attrs = { ...node.attrs }; - attrs.visibility = !attrs.visibility; - view.dispatch(view.state.tr.setNodeMarkup(y, undefined, attrs)); - let mark = view.state.schema.mark(view.state.schema.marks.highlight); - view.dispatch(view.state.tr.setSelection(TextSelection.create(view.state.doc, y + 1, y + 1))); - const from = view.state.selection.from; - let size = node.attrs.text.size; - view.dispatch(view.state.tr.replaceSelection(node.attrs.text).addMark(from, from + size, mark).removeStoredMark(mark)); - self._collapsed.textContent = "㊀"; + + 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() { } + selectNode() { } - updateSummarizedText(start?: any, mark?: any) { - let $start = this._view.state.doc.resolve(start); + deselectNode() { } + + className = (visible: boolean) => "formattedTextBox-summarizer" + (visible ? "" : "-collapsed"); + + updateSummarizedText(start?: any) { + let mark = this._view.state.schema.marks.highlight.create(); let endPos = start; - let _mark = this._view.state.schema.mark(this._view.state.schema.marks.highlight); let 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.includes(_mark)) { + if (node.marks.find((m: any) => m.type === mark.type)) { visited.add(node); endPos = i + node.nodeSize - 1; } @@ -644,10 +1005,7 @@ export class SummarizedView { } }); } - return { from: start, to: endPos }; - } - - deselectNode() { + return TextSelection.create(this._view.state.doc, start, endPos); } } // :: Schema |