aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/util/ProsemirrorExampleTransfer.ts13
-rw-r--r--src/client/util/RichTextSchema.tsx159
-rw-r--r--src/client/views/nodes/FormattedTextBox.scss47
-rw-r--r--src/client/views/nodes/FormattedTextBox.tsx30
4 files changed, 240 insertions, 9 deletions
diff --git a/src/client/util/ProsemirrorExampleTransfer.ts b/src/client/util/ProsemirrorExampleTransfer.ts
index bcb8b404b..419311df8 100644
--- a/src/client/util/ProsemirrorExampleTransfer.ts
+++ b/src/client/util/ProsemirrorExampleTransfer.ts
@@ -51,6 +51,19 @@ export default function buildKeymap<S extends Schema<any>>(schema: S, mapKeys?:
bind("Ctrl->", wrapIn(schema.nodes.blockquote));
+ bind("^", (state: EditorState<S>, dispatch: (tx: Transaction<S>) => void) => {
+ let newNode = schema.nodes.footnote.create({});
+ if (dispatch && state.selection.from === state.selection.to) {
+ let tr = state.tr;
+ tr.replaceSelectionWith(newNode); // replace insertion with a footnote.
+ dispatch(tr.setSelection(new NodeSelection( // select the footnote node to open its display
+ tr.doc.resolve( // get the location of the footnote node by subtracting the nodesize of the footnote from the current insertion point anchor (which will be immediately after the footnote node)
+ tr.selection.anchor - tr.selection.$anchor.nodeBefore!.nodeSize))));
+ return true;
+ }
+ return false;
+ })
+
let cmd = chainCommands(exitCode, (state, dispatch) => {
if (dispatch) {
diff --git a/src/client/util/RichTextSchema.tsx b/src/client/util/RichTextSchema.tsx
index 7911cf629..25d972857 100644
--- a/src/client/util/RichTextSchema.tsx
+++ b/src/client/util/RichTextSchema.tsx
@@ -1,7 +1,12 @@
import { DOMOutputSpecArray, MarkSpec, Node, NodeSpec, Schema, Slice } from "prosemirror-model";
import { bulletList, listItem, orderedList } from 'prosemirror-schema-list';
-import { TextSelection } from "prosemirror-state";
+import { TextSelection, EditorState } from "prosemirror-state";
import { Doc } from "../../new_fields/Doc";
+import { StepMap } from "prosemirror-transform";
+import { EditorView } from "prosemirror-view";
+import { keymap } from "prosemirror-keymap";
+import { undo, redo } from "prosemirror-history";
+import { toggleMark, splitBlock, selectAll, baseKeymap } from "prosemirror-commands";
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,6 +19,20 @@ export const nodes: { [index: string]: NodeSpec } = {
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" }]
+ },
+
// :: NodeSpec A plain paragraph textblock. Represented in the DOM
// as a `<p>` element.
paragraph: {
@@ -177,7 +196,7 @@ export const nodes: { [index: string]: NodeSpec } = {
group: 'block',
attrs: {
bulletStyle: { default: 0 },
- mapStyle: { default: "decimal" }
+ mapStyle: { default: "decimal" },
},
toDOM(node: Node<any>) {
const bs = node.attrs.bulletStyle;
@@ -186,7 +205,8 @@ export const nodes: { [index: string]: NodeSpec } = {
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]
+ //return node.attrs.bulletStyle < 2 ? ['ol', { class: `${map}-ol`, style: `list-style: none;` }, 0] :
+ // ['ol', { class: `${node.attrs.bulletStyle}`, style: `list-style: ${node.attrs.bulletStyle}; font-size: 5px` }, "hello"];
}
},
//this doesn't currently work for some reason
@@ -313,10 +333,10 @@ export const marks: { [index: string]: MarkSpec } = {
},
highlight: {
- parseDOM: [{ style: 'color: blue' }],
+ parseDOM: [{ style: 'text-decoration: underline' }],
toDOM() {
return ['span', {
- style: 'color: blue'
+ style: 'text-decoration: underline; text-decoration-color: rgba(204, 206, 210, 0.92)'
}];
}
},
@@ -581,6 +601,133 @@ export class OrderedListView {
}
}
+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() {
+ if (!(this.outerView as any).isOverlay) return;
+ // 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 i = 0; i < transactions.length; i++) {
+ let steps = transactions[i].steps
+ for (let j = 0; j < steps.length; j++)
+ outerTr.step(steps[j].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;
@@ -648,7 +795,7 @@ export class SummarizedView {
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;
}
diff --git a/src/client/views/nodes/FormattedTextBox.scss b/src/client/views/nodes/FormattedTextBox.scss
index dd07f5924..8f47402c4 100644
--- a/src/client/views/nodes/FormattedTextBox.scss
+++ b/src/client/views/nodes/FormattedTextBox.scss
@@ -84,6 +84,53 @@
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;
+}
+
+.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;
+}
+
ol { counter-reset: deci1 0;}
.decimal1-ol {counter-reset: deci1; p { display: inline }; font-size: 24 }
.decimal2-ol {counter-reset: deci2; p { display: inline }; font-size: 18 }
diff --git a/src/client/views/nodes/FormattedTextBox.tsx b/src/client/views/nodes/FormattedTextBox.tsx
index 6232dd3ab..bb44c5ac6 100644
--- a/src/client/views/nodes/FormattedTextBox.tsx
+++ b/src/client/views/nodes/FormattedTextBox.tsx
@@ -22,7 +22,7 @@ import { DocumentManager } from '../../util/DocumentManager';
import { DragManager } from "../../util/DragManager";
import buildKeymap from "../../util/ProsemirrorExampleTransfer";
import { inpRules } from "../../util/RichTextRules";
-import { ImageResizeView, schema, SummarizedView, OrderedListView } from "../../util/RichTextSchema";
+import { ImageResizeView, schema, SummarizedView, OrderedListView, FootnoteView } from "../../util/RichTextSchema";
import { SelectionManager } from "../../util/SelectionManager";
import { TooltipLinkingMenu } from "../../util/TooltipLinkingMenu";
import { TooltipTextMenu } from "../../util/TooltipTextMenu";
@@ -168,10 +168,33 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
}
}
+ // this should be internal to prosemirror, but is needed
+ // here to make sure that footnote view nodes in the overlay editor
+ // get removed when they're not selected.
+ syncNodeSelection(view: any, sel: any) {
+ if (sel instanceof NodeSelection) {
+ var desc = view.docView.descAt(sel.from);
+ if (desc != view.lastSelectedViewDesc) {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
+ }
+ if (desc) { desc.selectNode(); }
+ view.lastSelectedViewDesc = desc;
+ }
+ } else {
+ if (view.lastSelectedViewDesc) {
+ view.lastSelectedViewDesc.deselectNode();
+ view.lastSelectedViewDesc = null;
+ }
+ }
+ }
+
dispatchTransaction = (tx: Transaction) => {
if (this._editorView) {
const state = this._editorView.state.apply(tx);
this._editorView.updateState(state);
+ this.syncNodeSelection(this._editorView, this._editorView.state.selection); // bcz: ugh -- shouldn't be needed but without this the overlay view's footnote popup doesn't get deselected
if (state.selection.empty && FormattedTextBox._toolTipTextMenu && tx.storedMarks) {
FormattedTextBox._toolTipTextMenu.mark_key_pressed(tx.storedMarks);
}
@@ -612,12 +635,13 @@ export class FormattedTextBox extends DocComponent<(FieldViewProps & FormattedTe
nodeViews: {
image(node, view, getPos) { return new ImageResizeView(node, view, getPos); },
star(node, view, getPos) { return new SummarizedView(node, view, getPos); },
- ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); }
-
+ ordered_list(node, view, getPos) { return new OrderedListView(node, view, getPos); },
+ footnote(node, view, getPos) { return new FootnoteView(node, view, getPos) }
},
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
+ (this._editorView as any).isOverlay = this.props.isOverlay;
if (startup) {
Doc.GetProto(doc).documentText = undefined;
this._editorView.dispatch(this._editorView.state.tr.insertText(startup));