UNPKG

prosemirror-flat-list

Version:
212 lines (180 loc) 5.77 kB
import { chainCommands } from 'prosemirror-commands' import { Fragment, NodeRange, type Node as ProsemirrorNode, Slice, } from 'prosemirror-model' import { type Command, type EditorState, Selection, TextSelection, type Transaction, } from 'prosemirror-state' import { canSplit } from 'prosemirror-transform' import type { ListAttributes } from '../types' import { withAutoFixList } from '../utils/auto-fix-list' import { createAndFill } from '../utils/create-and-fill' import { isBlockNodeSelection } from '../utils/is-block-node-selection' import { isListNode } from '../utils/is-list-node' import { isTextSelection } from '../utils/is-text-selection' import { dedentNodeRange } from './dedent-list' import { enterWithoutLift } from './enter-without-lift' /** * Returns a command that split the current list node. * * @public @group Commands * */ export function createSplitListCommand(): Command { return withAutoFixList( chainCommands(splitBlockNodeSelectionInListCommand, splitListCommand), ) } function deriveListAttributes(listNode: ProsemirrorNode): ListAttributes { // For the new list node, we don't want to inherit any list attribute (For example: `checked`) other than `kind` return { kind: (listNode.attrs as ListAttributes).kind } } const splitBlockNodeSelectionInListCommand: Command = (state, dispatch) => { if (!isBlockNodeSelection(state.selection)) { return false } const selection = state.selection const { $to, node } = selection const parent = $to.parent // We only cover the case that // 1. the list node only contains one child node // 2. this child node is not a list node if ( isListNode(node) || !isListNode(parent) || parent.childCount !== 1 || parent.firstChild !== node ) { return false } const listType = parent.type const nextList = listType.createAndFill(deriveListAttributes(parent)) if (!nextList) { return false } if (dispatch) { const tr = state.tr const cutPoint = $to.pos tr.replace( cutPoint, cutPoint, new Slice(Fragment.fromArray([listType.create(), nextList]), 1, 1), ) const newSelection = TextSelection.near(tr.doc.resolve(cutPoint)) if (isTextSelection(newSelection)) { tr.setSelection(newSelection) dispatch(tr) } } return true } const splitListCommand: Command = (state, dispatch): boolean => { if (isBlockNodeSelection(state.selection)) { return false } const { $from, $to } = state.selection if (!$from.sameParent($to)) { return false } if ($from.depth < 2) { return false } const listDepth = $from.depth - 1 const listNode = $from.node(listDepth) if (!isListNode(listNode)) { return false } const parent = $from.parent const indexInList = $from.index(listDepth) const parentEmpty = parent.content.size === 0 // When the cursor is inside the first child of the list: // If the parent block is empty, dedent the list; // otherwise split and create a new list node. // When the cursor is inside the second or further children of the list: // Create a new paragraph. if (indexInList === 0) { if (parentEmpty) { const $listEnd = state.doc.resolve($from.end(listDepth)) const listParentDepth = listDepth - 1 const listParent = $from.node(listParentDepth) const indexInListParent = $from.index(listParentDepth) const isLastChildInListParent = indexInListParent === listParent.childCount - 1 // If the list is the last child of the list parent, we want to dedent // the whole list; otherwise, we only want to dedent the list content // (and thus unwrap these content from the list node) const range = isLastChildInListParent ? new NodeRange($from, $listEnd, listParentDepth) : new NodeRange($from, $listEnd, listDepth) const tr = state.tr if (range && dedentNodeRange(range, tr)) { dispatch?.(tr) return true } return false } else { return doSplitList(state, listNode, dispatch) } } else { if (parentEmpty) { return enterWithoutLift(state, dispatch) } else { return false } } } /** * @internal */ export function doSplitList( state: EditorState, listNode: ProsemirrorNode, dispatch?: (tr: Transaction) => void, ): boolean { const tr = state.tr const listType = listNode.type const attrs: ListAttributes = listNode.attrs const newAttrs: ListAttributes = deriveListAttributes(listNode) tr.delete(tr.selection.from, tr.selection.to) const { $from, $to } = tr.selection const { parentOffset } = $to const atStart = parentOffset == 0 const atEnd = parentOffset == $to.parent.content.size if (atStart) { if (dispatch) { const pos = $from.before(-1) tr.insert(pos, createAndFill(listType, newAttrs)) dispatch(tr.scrollIntoView()) } return true } if (atEnd && attrs.collapsed) { if (dispatch) { const pos = $from.after(-1) tr.insert(pos, createAndFill(listType, newAttrs)) tr.setSelection(Selection.near(tr.doc.resolve(pos))) dispatch(tr.scrollIntoView()) } return true } // If split the list at the start or at the middle, we want to inherit the // current parent type (e.g. heading); otherwise, we want to create a new // default block type (typically paragraph) const nextType = atEnd ? listNode.contentMatchAt(0).defaultType : undefined const typesAfter = [ { type: listType, attrs: newAttrs }, nextType ? { type: nextType } : null, ] if (!canSplit(tr.doc, $from.pos, 2, typesAfter)) { return false } dispatch?.(tr.split($from.pos, 2, typesAfter).scrollIntoView()) return true }