import { TextSelection } from 'prosemirror-state'; import { Attrs, Fragment, Node, Slice } from 'prosemirror-model'; import * as ReactDOM from 'react-dom/client'; import * as React from 'react'; import { EditorView } from 'prosemirror-view'; // currently nothing needs to be rendered for the internal view of a summary. export class SummaryViewInternal extends React.Component { render() { return null; } } // an elidable textblock that collapses when its '<-' is clicked and expands when its '...' anchor is clicked. // this node actively edits prosemirror (as opposed to just changing how things are rendered) and thus doesn't // really need a react view. However, it would be cleaner to figure out how to do this just as a react rendering // method instead of changing prosemirror's text when the expand/elide buttons are clicked. export class SummaryView { dom: HTMLSpanElement; // container for label and value root: ReactDOM.Root; constructor(node: Node, view: EditorView, getPos: () => number | undefined) { this.dom = document.createElement('span'); this.dom.className = this.className(node.attrs.visibility); this.dom.onpointerdown = (e: PointerEvent) => { this.onPointerDown(e, node, view, getPos); }; this.dom.onkeypress = function (e: KeyboardEvent) { e.stopPropagation(); }; this.dom.onkeydown = function (e: KeyboardEvent) { e.stopPropagation(); }; this.dom.onkeyup = function (e: KeyboardEvent) { e.stopPropagation(); }; this.dom.onmousedown = function (e: MouseEvent) { e.stopPropagation(); }; const js = node.toJSON; node.toJSON = function (...args: unknown[]) { return js.apply(this, args as []); }; this.root = ReactDOM.createRoot(this.dom); this.root.render(); } className = (visible: boolean) => 'formattedTextBox-summarizer' + (visible ? '' : '-collapsed'); destroy() { this.root.unmount(); } selectNode() {} updateSummarizedText(start: number, view: EditorView) { const mtype = view.state.schema.marks.summarize; const mtypeInc = view.state.schema.marks.summarizeInclusive; let endPos = start; const visited = new Set(); const summarized = new Set(); const isSummary = (node: Node) => summarized.has(node) || node.marks.find(m => m.type === mtype || m.type === mtypeInc); for (let i = start + 1; i < view.state.doc.nodeSize - 1; i++) { let skip = false; // eslint-disable-next-line no-loop-func view.state.doc.nodesBetween(start, i, (node: Node /* , pos: number, parent: Node, index: number */) => { isSummary(node) && Array.from(node.children).forEach(child => summarized.add(child)); if (node.isLeaf && !visited.has(node) && !skip) { if (summarized.has(node) || isSummary(node)) { visited.add(node); endPos = i + node.nodeSize - 1; } else skip = true; } }); } return TextSelection.create(view.state.doc, start, endPos); } onPointerDown = (e: PointerEvent, node: Node, view: EditorView, getPos: () => number | undefined) => { const visible = !node.attrs.visibility; const textSelection = visible // ? TextSelection.create(view.state.doc, (getPos() ?? 0) + 1) : this.updateSummarizedText((getPos() ?? 0) + 1, view); // update summarized text and save in attrs const text = textSelection.content(); const attrs = { ...node.attrs, visibility: visible, ...(!visible ? { text, textslice: text.toJSON() } : {}) } as Attrs; 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() ?? 0, undefined, attrs) ); // update the attrs e.preventDefault(); e.stopPropagation(); this.dom.className = this.className(visible); }; }