@tiptap/extension-node-range
Version:
node range extension for tiptap
306 lines (298 loc) • 10.6 kB
JavaScript
// src/node-range.ts
import { Extension } from "@tiptap/core";
import { Plugin, PluginKey } from "@tiptap/pm/state";
// src/helpers/getNodeRangeDecorations.ts
import { Decoration, DecorationSet } from "@tiptap/pm/view";
function getNodeRangeDecorations(ranges) {
if (!ranges.length) {
return DecorationSet.empty;
}
const decorations = [];
const doc = ranges[0].$from.node(0);
ranges.forEach((range) => {
const pos = range.$from.pos;
const node = range.$from.nodeAfter;
if (!node) {
return;
}
decorations.push(
Decoration.node(pos, pos + node.nodeSize, {
class: "ProseMirror-selectednoderange"
})
);
});
return DecorationSet.create(doc, decorations);
}
// src/helpers/getSelectionRanges.ts
import { NodeRange } from "@tiptap/pm/model";
import { SelectionRange } from "@tiptap/pm/state";
function getSelectionRanges($from, $to, depth) {
const ranges = [];
const doc = $from.node(0);
if (typeof depth === "number" && depth >= 0) {
} else if ($from.sameParent($to)) {
depth = Math.max(0, $from.sharedDepth($to.pos) - 1);
} else {
depth = $from.sharedDepth($to.pos);
}
const nodeRange = new NodeRange($from, $to, depth);
const offset = nodeRange.depth === 0 ? 0 : doc.resolve(nodeRange.start).posAtIndex(0);
nodeRange.parent.forEach((node, pos) => {
const from = offset + pos;
const to = from + node.nodeSize;
if (from < nodeRange.start || from >= nodeRange.end) {
return;
}
const selectionRange = new SelectionRange(doc.resolve(from), doc.resolve(to));
ranges.push(selectionRange);
});
return ranges;
}
// src/helpers/NodeRangeSelection.ts
import { Selection } from "@tiptap/pm/state";
// src/helpers/NodeRangeBookmark.ts
var NodeRangeBookmark = class _NodeRangeBookmark {
constructor(anchor, head) {
this.anchor = anchor;
this.head = head;
}
map(mapping) {
return new _NodeRangeBookmark(mapping.map(this.anchor), mapping.map(this.head));
}
resolve(doc) {
const $anchor = doc.resolve(this.anchor);
const $head = doc.resolve(this.head);
return new NodeRangeSelection($anchor, $head);
}
};
// src/helpers/NodeRangeSelection.ts
var NodeRangeSelection = class _NodeRangeSelection extends Selection {
constructor($anchor, $head, depth, bias = 1) {
const { doc } = $anchor;
const isCursor = $anchor === $head;
const isCursorAtEnd = $anchor.pos === doc.content.size && $head.pos === doc.content.size;
const $correctedHead = isCursor && !isCursorAtEnd ? doc.resolve($head.pos + (bias > 0 ? 1 : -1)) : $head;
const $correctedAnchor = isCursor && isCursorAtEnd ? doc.resolve($anchor.pos - (bias > 0 ? 1 : -1)) : $anchor;
const ranges = getSelectionRanges($correctedAnchor.min($correctedHead), $correctedAnchor.max($correctedHead), depth);
const $rangeFrom = $correctedHead.pos >= $anchor.pos ? ranges[0].$from : ranges[ranges.length - 1].$to;
const $rangeTo = $correctedHead.pos >= $anchor.pos ? ranges[ranges.length - 1].$to : ranges[0].$from;
super($rangeFrom, $rangeTo, ranges);
this.depth = depth;
}
// we can safely ignore this TypeScript error: https://github.com/Microsoft/TypeScript/issues/338
// @ts-ignore
get $to() {
return this.ranges[this.ranges.length - 1].$to;
}
eq(other) {
return other instanceof _NodeRangeSelection && other.$from.pos === this.$from.pos && other.$to.pos === this.$to.pos;
}
map(doc, mapping) {
const $anchor = doc.resolve(mapping.map(this.anchor));
const $head = doc.resolve(mapping.map(this.head));
return new _NodeRangeSelection($anchor, $head);
}
toJSON() {
return {
type: "nodeRange",
anchor: this.anchor,
head: this.head
};
}
get isForwards() {
return this.head >= this.anchor;
}
get isBackwards() {
return !this.isForwards;
}
extendBackwards() {
const { doc } = this.$from;
if (this.isForwards && this.ranges.length > 1) {
const ranges = this.ranges.slice(0, -1);
const $from2 = ranges[0].$from;
const $to = ranges[ranges.length - 1].$to;
return new _NodeRangeSelection($from2, $to, this.depth);
}
const firstRange = this.ranges[0];
const $from = doc.resolve(Math.max(0, firstRange.$from.pos - 1));
return new _NodeRangeSelection(this.$anchor, $from, this.depth);
}
extendForwards() {
const { doc } = this.$from;
if (this.isBackwards && this.ranges.length > 1) {
const ranges = this.ranges.slice(1);
const $from = ranges[0].$from;
const $to2 = ranges[ranges.length - 1].$to;
return new _NodeRangeSelection($to2, $from, this.depth);
}
const lastRange = this.ranges[this.ranges.length - 1];
const $to = doc.resolve(Math.min(doc.content.size, lastRange.$to.pos + 1));
return new _NodeRangeSelection(this.$anchor, $to, this.depth);
}
static fromJSON(doc, json) {
return new _NodeRangeSelection(doc.resolve(json.anchor), doc.resolve(json.head));
}
static create(doc, anchor, head, depth, bias = 1) {
return new this(doc.resolve(anchor), doc.resolve(head), depth, bias);
}
getBookmark() {
return new NodeRangeBookmark(this.anchor, this.head);
}
};
NodeRangeSelection.prototype.visible = false;
// src/helpers/isNodeRangeSelection.ts
function isNodeRangeSelection(value) {
return value instanceof NodeRangeSelection;
}
// src/node-range.ts
var NodeRange2 = Extension.create({
name: "nodeRange",
addOptions() {
return {
depth: void 0,
key: "Mod"
};
},
addKeyboardShortcuts() {
return {
// extend NodeRangeSelection upwards
"Shift-ArrowUp": ({ editor }) => {
const { depth } = this.options;
const { view, state } = editor;
const { doc, selection, tr } = state;
const { anchor, head } = selection;
if (!isNodeRangeSelection(selection)) {
const nodeRangeSelection2 = NodeRangeSelection.create(doc, anchor, head, depth, -1);
tr.setSelection(nodeRangeSelection2);
view.dispatch(tr);
return true;
}
const nodeRangeSelection = selection.extendBackwards();
tr.setSelection(nodeRangeSelection);
view.dispatch(tr);
return true;
},
// extend NodeRangeSelection downwards
"Shift-ArrowDown": ({ editor }) => {
const { depth } = this.options;
const { view, state } = editor;
const { doc, selection, tr } = state;
const { anchor, head } = selection;
if (!isNodeRangeSelection(selection)) {
const nodeRangeSelection2 = NodeRangeSelection.create(doc, anchor, head, depth);
tr.setSelection(nodeRangeSelection2);
view.dispatch(tr);
return true;
}
const nodeRangeSelection = selection.extendForwards();
tr.setSelection(nodeRangeSelection);
view.dispatch(tr);
return true;
},
// add `NodeRangeSelection` to all nodes
"Mod-a": ({ editor }) => {
const { depth } = this.options;
const { view, state } = editor;
const { doc, tr } = state;
const nodeRangeSelection = NodeRangeSelection.create(doc, 0, doc.content.size, depth);
tr.setSelection(nodeRangeSelection);
view.dispatch(tr);
return true;
}
};
},
onSelectionUpdate() {
const { selection } = this.editor.state;
if (isNodeRangeSelection(selection)) {
this.editor.view.dom.classList.add("ProseMirror-noderangeselection");
}
},
addProseMirrorPlugins() {
let hideTextSelection = false;
let activeMouseSelection = false;
return [
new Plugin({
key: new PluginKey("nodeRange"),
props: {
attributes: () => {
if (hideTextSelection) {
return {
class: "ProseMirror-noderangeselection"
};
}
return { class: "" };
},
handleDOMEvents: {
mousedown: (view, event) => {
const { key } = this.options;
const isMac = /Mac/.test(navigator.platform);
const isShift = !!event.shiftKey;
const isControl = !!event.ctrlKey;
const isAlt = !!event.altKey;
const isMeta = !!event.metaKey;
const isMod = isMac ? isMeta : isControl;
if (key === null || key === void 0 || key === "Shift" && isShift || key === "Control" && isControl || key === "Alt" && isAlt || key === "Meta" && isMeta || key === "Mod" && isMod) {
activeMouseSelection = true;
}
if (!activeMouseSelection) {
return false;
}
document.addEventListener(
"mouseup",
() => {
activeMouseSelection = false;
const { state } = view;
const { doc, selection, tr } = state;
const { $anchor, $head } = selection;
if ($anchor.sameParent($head)) {
return;
}
const nodeRangeSelection = NodeRangeSelection.create(doc, $anchor.pos, $head.pos, this.options.depth);
tr.setSelection(nodeRangeSelection);
view.dispatch(tr);
},
{ once: true }
);
return false;
}
},
// when selecting some text we want to render some decorations
// to preview a `NodeRangeSelection`
decorations: (state) => {
const { selection } = state;
const isNodeRange = isNodeRangeSelection(selection);
hideTextSelection = false;
if (!activeMouseSelection) {
if (!isNodeRange) {
return null;
}
hideTextSelection = true;
return getNodeRangeDecorations(selection.ranges);
}
const { $from, $to } = selection;
if (!isNodeRange && $from.sameParent($to)) {
return null;
}
const nodeRanges = getSelectionRanges($from, $to, this.options.depth);
if (!nodeRanges.length) {
return null;
}
hideTextSelection = true;
return getNodeRangeDecorations(nodeRanges);
}
}
})
];
}
});
// src/index.ts
var index_default = NodeRange2;
export {
NodeRange2 as NodeRange,
NodeRangeSelection,
index_default as default,
getNodeRangeDecorations,
getSelectionRanges,
isNodeRangeSelection
};
//# sourceMappingURL=index.js.map