diff --git a/packages/core/src/commands/splitListItem.ts b/packages/core/src/commands/splitListItem.ts index c05da4cb..a55e045f 100644 --- a/packages/core/src/commands/splitListItem.ts +++ b/packages/core/src/commands/splitListItem.ts @@ -1,13 +1,123 @@ -import { splitListItem as originalSplitListItem } from 'prosemirror-schema-list' -import { NodeType } from 'prosemirror-model' +import { + NodeType, + Node as ProseMirrorNode, + Fragment, + Slice, +} from 'prosemirror-model' +import { canSplit } from 'prosemirror-transform' import { Command } from '../types' import getNodeType from '../helpers/getNodeType' /** * Splits one list item into two list items. */ -export const splitListItem = (typeOrName: string | NodeType): Command => ({ state, dispatch }) => { +export const splitListItem = (typeOrName: string | NodeType): Command => ({ state, dispatch, editor }) => { const type = getNodeType(typeOrName, state.schema) + const { $from, $to } = state.selection - return originalSplitListItem(type)(state, dispatch) + // @ts-ignore + // eslint-disable-next-line + const node: ProseMirrorNode = state.selection.node + + if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) { + return false + } + + const grandParent = $from.node(-1) + + if (grandParent.type !== type) { + return false + } + + if ($from.parent.content.size === 0 && $from.node(-1).childCount === $from.indexAfter(-1)) { + // In an empty block. If this is a nested list, the wrapping + // list item should be split. Otherwise, bail out and let next + // command handle lifting. + if ( + $from.depth === 2 + || $from.node(-3).type !== type + || $from.index(-2) !== $from.node(-2).childCount - 1 + ) { + return false + } + + if (dispatch) { + let wrap = Fragment.empty + const keepItem = $from.index(-1) > 0 + + // Build a fragment containing empty versions of the structure + // from the outer list item to the parent node of the cursor + for (let d = $from.depth - (keepItem ? 1 : 2); d >= $from.depth - 3; d -= 1) { + wrap = Fragment.from($from.node(d).copy(wrap)) + } + + // Add a second list item with an empty default start node + // @ts-ignore + wrap = wrap.append(Fragment.from(type.createAndFill())) + + const tr = state.tr.replace( + $from.before(keepItem ? undefined : -1), + $from.after(-3), + new Slice(wrap, keepItem ? 3 : 2, 2), + ) + + tr + // @ts-ignore + .setSelection(state.selection.constructor.near(tr.doc.resolve($from.pos + (keepItem ? 3 : 2)))) + .scrollIntoView() + } + + return true + } + + const nextType = $to.pos === $from.end() + ? grandParent.contentMatchAt(0).defaultType + : null + + const extensionAttributes = editor.extensionManager.attributes + const currentTypeAttributes = grandParent.attrs + const currentNextTypeAttributes = $from.node().attrs + const newTypeAttributes = Object.fromEntries(Object + .entries(currentTypeAttributes) + .filter(([name]) => { + const extensionAttribute = extensionAttributes.find(item => { + return item.type === grandParent.type.name && item.name === name + }) + + if (!extensionAttribute) { + return false + } + + return extensionAttribute.attribute.keepOnSplit + })) + const newNextTypeAttributes = Object.fromEntries(Object + .entries(currentNextTypeAttributes) + .filter(([name]) => { + const extensionAttribute = extensionAttributes.find(item => { + return item.type === $from.node().type.name && item.name === name + }) + + if (!extensionAttribute) { + return false + } + + return extensionAttribute.attribute.keepOnSplit + })) + + const tr = state.tr.delete($from.pos, $to.pos) + const types = nextType + ? [{ type, attrs: newTypeAttributes }, { type: nextType, attrs: newNextTypeAttributes }] + : [{ type, attrs: newTypeAttributes }] + + // @ts-ignore + if (!canSplit(tr.doc, $from.pos, 2, nextType && [null])) { + return false + } + + if (dispatch) { + // @ts-ignore + tr.split($from.pos, 2, types).scrollIntoView() + } + + return true } diff --git a/packages/extension-task-item/src/task-item.ts b/packages/extension-task-item/src/task-item.ts index d1d1efd0..cfbf2402 100644 --- a/packages/extension-task-item/src/task-item.ts +++ b/packages/extension-task-item/src/task-item.ts @@ -34,6 +34,7 @@ export const TaskItem = Node.create({ renderHTML: attributes => ({ 'data-checked': attributes.checked, }), + keepOnSplit: false, }, } },