UNPKG

prosemirror-better-backspace

Version:

A ProseMirror plugin that provides better backspace behavior for list items and empty paragraphs

98 lines 4.14 kB
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