@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
90 lines (72 loc) • 2.92 kB
text/typescript
import { Fragment, Node, ResolvedPos, Slice } from "prosemirror-model";
import { Selection } from "prosemirror-state";
import { Mappable } from "prosemirror-transform";
/**
* This class represents an editor selection which spans multiple nodes/blocks. It's currently only used to allow users
* to drag multiple blocks at the same time. Expects the selection anchor and head to be between nodes, i.e. just before
* the first target node and just after the last, and that anchor and head are at the same nesting level.
*
* Partially based on ProseMirror's NodeSelection implementation:
* (https://github.com/ProseMirror/prosemirror-state/blob/master/src/selection.ts)
* MultipleNodeSelection differs from NodeSelection in the following ways:
* 1. Stores which nodes are included in the selection instead of just a single node.
* 2. Already expects the selection to start just before the first target node and ends just after the last, while a
* NodeSelection automatically sets both anchor and head to just before the single target node.
*/
export class MultipleNodeSelection extends Selection {
nodes: Array<Node>;
constructor($anchor: ResolvedPos, $head: ResolvedPos) {
super($anchor, $head);
// Parent is at the same nesting level as anchor/head since they are just before/ just after target nodes.
const parentNode = $anchor.node();
this.nodes = [];
$anchor.doc.nodesBetween($anchor.pos, $head.pos, (node, _pos, parent) => {
if (parent !== null && parent.eq(parentNode)) {
this.nodes.push(node);
return false;
}
return;
});
}
static create(doc: Node, from: number, to = from): MultipleNodeSelection {
return new MultipleNodeSelection(doc.resolve(from), doc.resolve(to));
}
content(): Slice {
return new Slice(Fragment.from(this.nodes), 0, 0);
}
eq(selection: Selection): boolean {
if (!(selection instanceof MultipleNodeSelection)) {
return false;
}
if (this.nodes.length !== selection.nodes.length) {
return false;
}
if (this.from !== selection.from || this.to !== selection.to) {
return false;
}
for (let i = 0; i < this.nodes.length; i++) {
if (!this.nodes[i].eq(selection.nodes[i])) {
return false;
}
}
return true;
}
map(doc: Node, mapping: Mappable): Selection {
const fromResult = mapping.mapResult(this.from);
const toResult = mapping.mapResult(this.to);
if (toResult.deleted) {
return Selection.near(doc.resolve(fromResult.pos));
}
if (fromResult.deleted) {
return Selection.near(doc.resolve(toResult.pos));
}
return new MultipleNodeSelection(
doc.resolve(fromResult.pos),
doc.resolve(toResult.pos),
);
}
toJSON(): any {
return { type: "multiple-node", anchor: this.anchor, head: this.head };
}
}
Selection.jsonID("multiple-node", MultipleNodeSelection);