import { getNodeType, RawCommands } from '@tiptap/core'; import { Node, NodeType } from '@tiptap/pm/model'; import { EditorState } from '@tiptap/pm/state'; /** * Finds the first node of a given type or name in the current selection. * @param state The editor state. * @param typeOrName The node type or name. * @param pos The position to start searching from. * @param maxDepth The maximum depth to search. * @returns The node and the depth as an array. */ export const getNodeAtPosition = ( state: EditorState, typeOrName: string | NodeType, pos: number, maxDepth = 20 ) => { const $pos = state.doc.resolve(pos); let currentDepth = maxDepth; let node: Node | null = null; while (currentDepth > 0 && node === null) { const currentNode = $pos.node(currentDepth); if (currentNode?.type.name === typeOrName) { node = currentNode; } else { currentDepth -= 1; } } return [node, currentDepth] as [Node | null, number]; }; export const isAtStartOfNode = (state: EditorState) => { const { $from, $to } = state.selection; if ($from.parentOffset > 0 || $from.pos !== $to.pos) { return false; } return true; }; export const findListItemPos = ( typeOrName: string | NodeType, state: EditorState ) => { const { $from } = state.selection; const nodeType = getNodeType(typeOrName, state.schema); let currentNode = null; let currentDepth = $from.depth; let currentPos = $from.pos; let targetDepth: number | null = null; while (currentDepth > 0 && targetDepth === null) { currentNode = $from.node(currentDepth); if (currentNode.type === nodeType) { targetDepth = currentDepth; } else { currentDepth -= 1; currentPos -= 1; } } if (targetDepth === null) { return null; } return { $pos: state.doc.resolve(currentPos), depth: targetDepth }; }; export const hasPreviousListItem = (typeOrName: string, state: EditorState) => { const listItemPos = findListItemPos(typeOrName, state); if (!listItemPos) { return false; } const $item = state.doc.resolve(listItemPos.$pos.pos); const $prev = state.doc.resolve(listItemPos.$pos.pos - 2); const prevNode = $prev.node($item.depth); if (!prevNode) { return false; } return prevNode.type.name === typeOrName; }; export const listItemHasSubList = ( typeOrName: string, state: EditorState, node?: Node ) => { if (!node) { return false; } const nodeType = getNodeType(typeOrName, state.schema); let hasSubList = false; node.descendants((child) => { if (child.type === nodeType) { hasSubList = true; } }); return hasSubList; }; export const isAtEndOfNode = (state: EditorState) => { const { $from, $to } = state.selection; if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) { return false; } return true; }; export const getNextListDepth = (typeOrName: string, state: EditorState) => { const listItemPos = findListItemPos(typeOrName, state); if (!listItemPos) { return false; } const [, depth] = getNodeAtPosition( state, typeOrName, listItemPos.$pos.pos + 4 ); return depth; }; export const nextListIsDeeper = (typeOrName: string, state: EditorState) => { const listDepth = getNextListDepth(typeOrName, state); const listItemPos = findListItemPos(typeOrName, state); if (!listItemPos || !listDepth) { return false; } if (listDepth > listItemPos.depth) { return true; } return false; }; export const nextListIsHigher = (typeOrName: string, state: EditorState) => { const listDepth = getNextListDepth(typeOrName, state); const listItemPos = findListItemPos(typeOrName, state); if (!listItemPos || !listDepth) { return false; } if (listDepth < listItemPos.depth) { return true; } return false; };