@blocknote/core
Version:
A "Notion-style" block-based extensible text editor built on top of Prosemirror and Tiptap.
115 lines (101 loc) • 3.81 kB
text/typescript
import { Mapping } from "prosemirror-transform";
import {
absolutePositionToRelativePosition,
relativePositionToAbsolutePosition,
ySyncPluginKey,
} from "y-prosemirror";
import type { BlockNoteEditor } from "../editor/BlockNoteEditor.js";
import * as Y from "yjs";
import type { ProsemirrorBinding } from "y-prosemirror";
/**
* This is used to track a mapping for each editor. The mapping stores the mappings for each transaction since the first transaction that was tracked.
*/
const editorToMapping = new Map<BlockNoteEditor<any, any, any>, Mapping>();
/**
* This initializes a single mapping for an editor instance.
*/
function getMapping(editor: BlockNoteEditor<any, any, any>) {
if (editorToMapping.has(editor)) {
// Mapping already initialized, so we don't need to do anything
return editorToMapping.get(editor)!;
}
const mapping = new Mapping();
editor._tiptapEditor.on("transaction", ({ transaction }) => {
mapping.appendMapping(transaction.mapping);
});
editor._tiptapEditor.on("destroy", () => {
// Cleanup the mapping when the editor is destroyed
editorToMapping.delete(editor);
});
// There only is one mapping per editor, so we can just set it
editorToMapping.set(editor, mapping);
return mapping;
}
/**
* This is used to keep track of positions of elements in the editor.
* It is needed because y-prosemirror's sync plugin can disrupt normal prosemirror position mapping.
*
* It is specifically made to be able to be used whether the editor is being used in a collaboratively, or single user, providing the same API.
*
* @param editor The editor to track the position of.
* @param position The position to track.
* @param side The side of the position to track. "left" is the default. "right" would move with the change if the change is in the right direction.
* @returns A function that returns the position of the element.
*/
export function trackPosition(
/**
* The editor to track the position of.
*/
editor: BlockNoteEditor<any, any, any>,
/**
* The position to track.
*/
position: number,
/**
* This is the side of the position to track. "left" is the default. "right" would move with the change if the change is in the right direction.
*/
side: "left" | "right" = "left",
): () => number {
const ySyncPluginState = ySyncPluginKey.getState(
editor._tiptapEditor.state,
) as {
doc: Y.Doc;
binding: ProsemirrorBinding;
};
if (!ySyncPluginState) {
// No y-prosemirror sync plugin, so we need to track the mapping manually
// This will initialize the mapping for this editor, if needed
const mapping = getMapping(editor);
// This is the start point of tracking the mapping
const trackedMapLength = mapping.maps.length;
return () => {
const pos = mapping
// Only read the history of the mapping that we care about
.slice(trackedMapLength)
.map(position, side === "left" ? -1 : 1);
return pos;
};
}
const relativePosition = absolutePositionToRelativePosition(
// Track the position after the position if we are on the right side
position + (side === "right" ? 1 : 0),
ySyncPluginState.binding.type,
ySyncPluginState.binding.mapping,
);
return () => {
const curYSyncPluginState = ySyncPluginKey.getState(
editor._tiptapEditor.state,
) as typeof ySyncPluginState;
const pos = relativePositionToAbsolutePosition(
curYSyncPluginState.doc,
curYSyncPluginState.binding.type,
relativePosition,
curYSyncPluginState.binding.mapping,
);
// This can happen if the element is garbage collected
if (pos === null) {
throw new Error("Position not found, cannot track positions");
}
return pos + (side === "right" ? -1 : 0);
};
}