prosemirror-better-backspace
Version:
A ProseMirror plugin that provides better backspace behavior for list items and empty paragraphs
98 lines • 4.14 kB
JavaScript
import { TextSelection } from "prosemirror-state";
import { Fragment } from "prosemirror-model";
import { keymap } from "prosemirror-keymap";
import { liftListItem } from "prosemirror-schema-list";
/**
* Finds the nearest previous node that can contain text nodes.
* @param {ResolvedPos} $pos - The resolved position to start from.
* @param {Schema} schema - The ProseMirror schema.
* @returns {{ node: ProseMirrorNode, pos: number } | null}
*/
function findNearestPreviousTextContainer($pos, schema) {
for (let i = $pos.pos - 1; i >= 0; i--) {
const $current = $pos.doc.resolve(i);
if ($current.nodeBefore) {
const nodeType = $current.nodeBefore.type;
const contentExpr = nodeType.spec.content;
// Check if this node can contain text nodes
// A node can contain text if its content expression allows "text" or "inline"
if (contentExpr &&
(contentExpr.includes("text") || contentExpr.includes("inline"))) {
return {
node: $current.nodeBefore,
pos: i - $current.nodeBefore.nodeSize + 1,
};
}
}
}
return null;
}
/**
* Gets the closest parent node that matches the predicate.
* @param {ResolvedPos} pos - The resolved position to start from.
* @param {(node: Node) => boolean} predicate - Function to test each node.
* @returns {[Node, number] | undefined} - The matching node and its position, or undefined.
*/
const getClosestParent = (pos, predicate) => {
const node = pos.node();
if (node?.isText || !node) {
return undefined;
}
if (predicate(node)) {
return [node, pos.start(pos.depth - 1)];
}
// Get the parent position and recursively check
const parentPos = pos.parent;
if (parentPos) {
const parentResolvedPos = pos.doc.resolve(pos.start(pos.depth - 1));
return getClosestParent(parentResolvedPos, predicate);
}
return undefined;
};
/**
* Custom backspace handler that:
* 1. Lifts list items when backspacing on empty list items
* 2. Merges empty paragraphs with previous text containers
*
* @param {Schema} schema - The ProseMirror schema.
* @returns {Plugin} A ProseMirror plugin with the custom backspace behavior.
*/
export function createBackspacePlugin(schema) {
return keymap({
Backspace: (state, dispatch) => {
const { $from, empty } = state.selection;
const fromNode = $from.node();
const isListItem = (node) => node.type.name === "list_item";
const parentListItem = $from.depth > 1 ? getClosestParent($from, isListItem) : undefined;
const hasParentList = Boolean(parentListItem);
if (!empty || $from.parentOffset !== 0 || $from.pos < 2)
return false;
/**
* If we're in a list item, lift it up
*/
if (hasParentList) {
liftListItem(schema.nodes.list_item)(state, dispatch);
return true;
}
const nearestTextContainerNode = findNearestPreviousTextContainer($from, schema);
if (!nearestTextContainerNode)
return false;
const textFragment = Fragment.from(fromNode.children);
const nearestTextEnd = nearestTextContainerNode.pos +
nearestTextContainerNode.node.textContent.length;
// For root paragraphs, we need to delete the entire paragraph node
const fromStart = $from.start($from.depth);
const fromEnd = $from.end($from.depth);
const tr = state.tr
.deleteRange(fromStart - 1, fromEnd + 1)
.insert(nearestTextContainerNode.pos +
nearestTextContainerNode.node.textContent.length, textFragment);
const selection = TextSelection.create(tr.doc, nearestTextEnd);
if (!selection)
return false;
dispatch?.(tr.setSelection(selection));
return true;
},
});
}
//# sourceMappingURL=backspace-plugin.js.map