From e1f859ea203b6d62deb43aff41827beb6d228790 Mon Sep 17 00:00:00 2001 From: bdbch Date: Wed, 12 Jul 2023 12:05:17 +0200 Subject: [PATCH 1/4] add support for mod-delete and mod-backspace --- packages/core/src/helpers/isAtEndOfNode.ts | 2 +- .../src/handlers/handleBackspace.ts | 45 ++++++++++ .../src/handlers/handleDelete.ts | 37 +++++++++ packages/extension-list-item/src/list-item.ts | 83 ++----------------- 4 files changed, 90 insertions(+), 77 deletions(-) create mode 100644 packages/extension-list-item/src/handlers/handleBackspace.ts create mode 100644 packages/extension-list-item/src/handlers/handleDelete.ts diff --git a/packages/core/src/helpers/isAtEndOfNode.ts b/packages/core/src/helpers/isAtEndOfNode.ts index 3dd8fc3602d..9ea038efe6e 100644 --- a/packages/core/src/helpers/isAtEndOfNode.ts +++ b/packages/core/src/helpers/isAtEndOfNode.ts @@ -1,6 +1,6 @@ import { EditorState } from '@tiptap/pm/state' -export const istAtEndOfNode = (state: EditorState) => { +export const isAtEndOfNode = (state: EditorState) => { const { $from, $to } = state.selection if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) { diff --git a/packages/extension-list-item/src/handlers/handleBackspace.ts b/packages/extension-list-item/src/handlers/handleBackspace.ts new file mode 100644 index 00000000000..47f69391af8 --- /dev/null +++ b/packages/extension-list-item/src/handlers/handleBackspace.ts @@ -0,0 +1,45 @@ +import { Editor, isAtStartOfNode, isNodeActive } from '@tiptap/core' + +import { + findListItemPos, hasListItemBefore, listItemHasSubList, +} from '../helpers/index.js' + +export const handleBackspace = (editor: Editor, name: string) => { + // this is required to still handle the undo handling + if (editor.commands.undoInputRule()) { + return true + } + + // if the cursor is not inside the current node type + // do nothing and proceed + if (!isNodeActive(editor.state, name)) { + return false + } + + // if the cursor is not at the start of a node + // do nothing and proceed + if (!isAtStartOfNode(editor.state)) { + return false + } + + const listItemPos = findListItemPos(name, editor.state) + + if (!listItemPos) { + return false + } + + const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2) + const prevNode = $prev.node(listItemPos.depth) + + const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode) + + // if the previous item is a list item and doesn't have a sublist, join the list items + if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) { + return editor.commands.joinListItemBackward(name) + } + + // otherwise in the end, a backspace should + // always just lift the list item if + // joining / merging is not possible + return editor.chain().liftListItem(name).run() +} diff --git a/packages/extension-list-item/src/handlers/handleDelete.ts b/packages/extension-list-item/src/handlers/handleDelete.ts new file mode 100644 index 00000000000..a395b851ff9 --- /dev/null +++ b/packages/extension-list-item/src/handlers/handleDelete.ts @@ -0,0 +1,37 @@ +import { Editor, isAtEndOfNode, isNodeActive } from '@tiptap/core' + +import { nextListIsDeeper, nextListIsHigher } from '../helpers/index.js' + +export const handleDelete = (editor: Editor, name: string) => { + // if the cursor is not inside the current node type + // do nothing and proceed + if (!isNodeActive(editor.state, name)) { + return false + } + + // if the cursor is not at the end of a node + // do nothing and proceed + if (!isAtEndOfNode(editor.state)) { + return false + } + + // check if the next node is a list with a deeper depth + if (nextListIsDeeper(name, editor.state)) { + return editor + .chain() + .focus(editor.state.selection.from + 4) + .lift(name) + .joinBackward() + .run() + } + + if (nextListIsHigher(name, editor.state)) { + return editor.chain() + .joinForward() + .joinBackward() + .run() + } + + // check if the next node is also a listItem + return editor.commands.joinListItemForward(name) +} diff --git a/packages/extension-list-item/src/list-item.ts b/packages/extension-list-item/src/list-item.ts index 0aa46d18ccb..6ddd102dc9c 100644 --- a/packages/extension-list-item/src/list-item.ts +++ b/packages/extension-list-item/src/list-item.ts @@ -1,13 +1,12 @@ import { - isAtStartOfNode, isNodeActive, istAtEndOfNode, mergeAttributes, Node, + mergeAttributes, Node, } from '@tiptap/core' import { NodeType } from '@tiptap/pm/model' import { joinListItemBackward } from './commands/joinListItemBackward.js' import { joinListItemForward } from './commands/joinListItemForward.js' -import { - findListItemPos, hasListItemBefore, listItemHasSubList, nextListIsDeeper, nextListIsHigher, -} from './helpers/index.js' +import { handleBackspace } from './handlers/handleBackspace.js' +import { handleDelete } from './handlers/handleDelete.js' declare module '@tiptap/core' { interface Commands { @@ -62,78 +61,10 @@ export const ListItem = Node.create({ Enter: () => this.editor.commands.splitListItem(this.name), Tab: () => this.editor.commands.sinkListItem(this.name), 'Shift-Tab': () => this.editor.commands.liftListItem(this.name), - Delete: ({ editor }) => { - // if the cursor is not inside the current node type - // do nothing and proceed - if (!isNodeActive(editor.state, this.name)) { - return false - } - - // if the cursor is not at the end of a node - // do nothing and proceed - if (!istAtEndOfNode(editor.state)) { - return false - } - - // check if the next node is a list with a deeper depth - if (nextListIsDeeper(this.name, editor.state)) { - return editor - .chain() - .focus(editor.state.selection.from + 4) - .lift(this.name) - .joinBackward() - .run() - } - - if (nextListIsHigher(this.name, editor.state)) { - return editor.chain() - .joinForward() - .joinBackward() - .run() - } - - // check if the next node is also a listItem - return editor.commands.joinListItemForward(this.name) - }, - Backspace: ({ editor }) => { - // this is required to still handle the undo handling - if (this.editor.commands.undoInputRule()) { - return true - } - - // if the cursor is not inside the current node type - // do nothing and proceed - if (!isNodeActive(editor.state, this.name)) { - return false - } - - // if the cursor is not at the start of a node - // do nothing and proceed - if (!isAtStartOfNode(editor.state)) { - return false - } - - const listItemPos = findListItemPos(this.name, editor.state) - - if (!listItemPos) { - return false - } - - const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2) - const prevNode = $prev.node(listItemPos.depth) - - const previousListItemHasSubList = listItemHasSubList(this.name, editor.state, prevNode) - - // if the previous item is a list item and doesn't have a sublist, join the list items - if (hasListItemBefore(this.name, editor.state) && !previousListItemHasSubList) { - return editor.commands.joinListItemBackward(this.name) - } - - // otherwise in the end, a backspace should - // always just lift the list item if - // joining / merging is not possible - return editor.chain().liftListItem(this.name).run() - }, + Delete: ({ editor }) => handleDelete(editor, this.name), + 'Mod-Delete': ({ editor }) => handleDelete(editor, this.name), + Backspace: ({ editor }) => handleBackspace(editor, this.name), + 'Mod-Backspace': ({ editor }) => handleBackspace(editor, this.name), } }, }) From 20d7fd737f0b81c5ba4e0d815457ab1f5f12f706 Mon Sep 17 00:00:00 2001 From: bdbch Date: Thu, 13 Jul 2023 12:10:25 +0200 Subject: [PATCH 2/4] fix backspace not working right behind a list --- packages/core/src/commands/cut.ts | 20 ++++++------ .../src/handlers/handleBackspace.ts | 31 ++++++++++++++++++- .../src/helpers/hasListBefore.ts | 15 +++++++++ packages/extension-list-item/src/list-item.ts | 8 +++-- 4 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 packages/extension-list-item/src/helpers/hasListBefore.ts diff --git a/packages/core/src/commands/cut.ts b/packages/core/src/commands/cut.ts index e07247b643f..2a6ff7f3a66 100644 --- a/packages/core/src/commands/cut.ts +++ b/packages/core/src/commands/cut.ts @@ -1,3 +1,5 @@ +import { TextSelection } from '@tiptap/pm/state' + import { RawCommands } from '../types.js' declare module '@tiptap/core' { @@ -11,17 +13,17 @@ declare module '@tiptap/core' { } } -export const cut: RawCommands['cut'] = (originRange, targetPos) => ({ editor }) => { +export const cut: RawCommands['cut'] = (originRange, targetPos) => ({ editor, tr }) => { const { state } = editor const contentSlice = state.doc.slice(originRange.from, originRange.to) - return editor - .chain() - .deleteRange(originRange) - .command(({ commands, tr }) => { - return commands.insertContentAt(tr.mapping.map(targetPos), contentSlice.content.toJSON()) - }) - .focus() - .run() + tr.deleteRange(originRange.from, originRange.to) + const newPos = tr.mapping.map(targetPos) + + tr.insert(newPos, contentSlice.content) + + tr.setSelection(new TextSelection(tr.doc.resolve(newPos - 1))) + + return true } diff --git a/packages/extension-list-item/src/handlers/handleBackspace.ts b/packages/extension-list-item/src/handlers/handleBackspace.ts index 47f69391af8..7ea95226f22 100644 --- a/packages/extension-list-item/src/handlers/handleBackspace.ts +++ b/packages/extension-list-item/src/handlers/handleBackspace.ts @@ -1,15 +1,44 @@ import { Editor, isAtStartOfNode, isNodeActive } from '@tiptap/core' +import { Node } from '@tiptap/pm/model' +import { hasListBefore } from '../helpers/hasListBefore.js' import { findListItemPos, hasListItemBefore, listItemHasSubList, } from '../helpers/index.js' -export const handleBackspace = (editor: Editor, name: string) => { +export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => { // this is required to still handle the undo handling if (editor.commands.undoInputRule()) { return true } + // if the current item is NOT inside a list item & + // the previous item is a list (orderedList or bulletList) + // move the cursor into the list and delete the current item + if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) { + const { $anchor } = editor.state.selection + + const $listPos = editor.state.doc.resolve($anchor.before() - 1) + + const listDescendants: Array<{ node: Node, pos: number }> = [] + + $listPos.node().descendants((node, pos) => { + if (node.type.name === name) { + listDescendants.push({ node, pos }) + } + }) + + const lastItem = listDescendants.at(-1) + + if (!lastItem) { + return false + } + + const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1) + + return editor.chain().cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end()).joinForward().run() + } + // if the cursor is not inside the current node type // do nothing and proceed if (!isNodeActive(editor.state, name)) { diff --git a/packages/extension-list-item/src/helpers/hasListBefore.ts b/packages/extension-list-item/src/helpers/hasListBefore.ts new file mode 100644 index 00000000000..f8ae974620e --- /dev/null +++ b/packages/extension-list-item/src/helpers/hasListBefore.ts @@ -0,0 +1,15 @@ +import { EditorState } from '@tiptap/pm/state' + +export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => { + const { $anchor } = editorState.selection + + const previousNodePos = Math.max(0, $anchor.pos - 2) + + const previousNode = editorState.doc.resolve(previousNodePos).node() + + if (!previousNode || !parentListTypes.includes(previousNode.type.name)) { + return false + } + + return true +} diff --git a/packages/extension-list-item/src/list-item.ts b/packages/extension-list-item/src/list-item.ts index 6ddd102dc9c..8515751f677 100644 --- a/packages/extension-list-item/src/list-item.ts +++ b/packages/extension-list-item/src/list-item.ts @@ -22,6 +22,8 @@ declare module '@tiptap/core' { export interface ListItemOptions { HTMLAttributes: Record, + bulletListTypeName: string + orderedListTypeName: string } export const ListItem = Node.create({ @@ -30,6 +32,8 @@ export const ListItem = Node.create({ addOptions() { return { HTMLAttributes: {}, + bulletListTypeName: 'bulletList', + orderedListTypeName: 'orderedList', } }, @@ -63,8 +67,8 @@ export const ListItem = Node.create({ 'Shift-Tab': () => this.editor.commands.liftListItem(this.name), Delete: ({ editor }) => handleDelete(editor, this.name), 'Mod-Delete': ({ editor }) => handleDelete(editor, this.name), - Backspace: ({ editor }) => handleBackspace(editor, this.name), - 'Mod-Backspace': ({ editor }) => handleBackspace(editor, this.name), + Backspace: ({ editor }) => handleBackspace(editor, this.name, [this.options.bulletListTypeName, this.options.orderedListTypeName]), + 'Mod-Backspace': ({ editor }) => handleBackspace(editor, this.name, [this.options.bulletListTypeName, this.options.orderedListTypeName]), } }, }) From 0f4944df54898610e13c7039f9a63e9a04c49847 Mon Sep 17 00:00:00 2001 From: bdbch Date: Thu, 13 Jul 2023 15:14:30 +0200 Subject: [PATCH 3/4] move list helpers to core, add support for task lists --- packages/core/src/commands/index.ts | 2 + .../src/commands/joinItemBackward.ts} | 16 +++- .../src/commands/joinItemForward.ts} | 16 +++- packages/core/src/extensions/keymap.ts | 2 + packages/core/src/helpers/index.ts | 1 + .../helpers/listHelpers/findListItemPos.ts} | 11 +-- .../helpers/listHelpers/getNextListDepth.ts | 16 ++++ .../helpers/listHelpers}/handleBackspace.ts | 14 ++-- .../src/helpers/listHelpers}/handleDelete.ts | 11 +-- .../src/helpers/listHelpers}/hasListBefore.ts | 0 .../helpers/listHelpers}/hasListItemAfter.ts | 0 .../helpers/listHelpers}/hasListItemBefore.ts | 0 .../core/src/helpers/listHelpers/index.ts | 10 +++ .../listHelpers/listItemHasSubList.ts} | 3 +- .../helpers/listHelpers/nextListIsDeeper.ts | 19 +++++ .../helpers/listHelpers/nextListIsHigher.ts | 19 +++++ .../extension-list-item/src/helpers/index.ts | 76 ------------------- packages/extension-list-item/src/list-item.ts | 26 +------ packages/extension-task-item/src/task-item.ts | 14 +++- 19 files changed, 132 insertions(+), 124 deletions(-) rename packages/{extension-list-item/src/commands/joinListItemBackward.ts => core/src/commands/joinItemBackward.ts} (52%) rename packages/{extension-list-item/src/commands/joinListItemForward.ts => core/src/commands/joinItemForward.ts} (53%) rename packages/{extension-list-item/src/helpers/getCurrentListItemPos.ts => core/src/helpers/listHelpers/findListItemPos.ts} (67%) create mode 100644 packages/core/src/helpers/listHelpers/getNextListDepth.ts rename packages/{extension-list-item/src/handlers => core/src/helpers/listHelpers}/handleBackspace.ts (83%) rename packages/{extension-list-item/src/handlers => core/src/helpers/listHelpers}/handleDelete.ts (70%) rename packages/{extension-list-item/src/helpers => core/src/helpers/listHelpers}/hasListBefore.ts (100%) rename packages/{extension-list-item/src/helpers => core/src/helpers/listHelpers}/hasListItemAfter.ts (100%) rename packages/{extension-list-item/src/helpers => core/src/helpers/listHelpers}/hasListItemBefore.ts (100%) create mode 100644 packages/core/src/helpers/listHelpers/index.ts rename packages/{extension-list-item/src/helpers/listItemhasSublist.ts => core/src/helpers/listHelpers/listItemHasSubList.ts} (89%) create mode 100644 packages/core/src/helpers/listHelpers/nextListIsDeeper.ts create mode 100644 packages/core/src/helpers/listHelpers/nextListIsHigher.ts delete mode 100644 packages/extension-list-item/src/helpers/index.ts diff --git a/packages/core/src/commands/index.ts b/packages/core/src/commands/index.ts index 954789fa5e6..16e071fea6c 100644 --- a/packages/core/src/commands/index.ts +++ b/packages/core/src/commands/index.ts @@ -17,6 +17,8 @@ export * from './forEach.js' export * from './insertContent.js' export * from './insertContentAt.js' export * from './join.js' +export * from './joinItemBackward.js' +export * from './joinItemForward.js' export * from './keyboardShortcut.js' export * from './lift.js' export * from './liftEmptyBlock.js' diff --git a/packages/extension-list-item/src/commands/joinListItemBackward.ts b/packages/core/src/commands/joinItemBackward.ts similarity index 52% rename from packages/extension-list-item/src/commands/joinListItemBackward.ts rename to packages/core/src/commands/joinItemBackward.ts index c0ab87014f1..8a9b6434f92 100644 --- a/packages/extension-list-item/src/commands/joinListItemBackward.ts +++ b/packages/core/src/commands/joinItemBackward.ts @@ -1,7 +1,19 @@ -import { RawCommands } from '@tiptap/core' import { joinPoint } from '@tiptap/pm/transform' -export const joinListItemBackward: RawCommands['splitListItem'] = () => ({ +import { RawCommands } from '../types.js' + +declare module '@tiptap/core' { + interface Commands { + joinItemBackward: { + /** + * Join two nodes Forwards. + */ + joinItemBackward: () => ReturnType + } + } +} + +export const joinItemBackward: RawCommands['joinItemBackward'] = () => ({ tr, state, dispatch, }) => { try { diff --git a/packages/extension-list-item/src/commands/joinListItemForward.ts b/packages/core/src/commands/joinItemForward.ts similarity index 53% rename from packages/extension-list-item/src/commands/joinListItemForward.ts rename to packages/core/src/commands/joinItemForward.ts index 22c5dc5a743..ddcb196b96e 100644 --- a/packages/extension-list-item/src/commands/joinListItemForward.ts +++ b/packages/core/src/commands/joinItemForward.ts @@ -1,7 +1,19 @@ -import { RawCommands } from '@tiptap/core' import { joinPoint } from '@tiptap/pm/transform' -export const joinListItemForward: RawCommands['splitListItem'] = () => ({ +import { RawCommands } from '../types.js' + +declare module '@tiptap/core' { + interface Commands { + joinItemForward: { + /** + * Join two nodes Forwards. + */ + joinItemForward: () => ReturnType + } + } +} + +export const joinItemForward: RawCommands['joinItemForward'] = () => ({ state, dispatch, tr, diff --git a/packages/core/src/extensions/keymap.ts b/packages/core/src/extensions/keymap.ts index dbcbfece63e..d16346fe506 100644 --- a/packages/core/src/extensions/keymap.ts +++ b/packages/core/src/extensions/keymap.ts @@ -12,6 +12,7 @@ export const Keymap = Extension.create({ addKeyboardShortcuts() { const handleBackspace = () => this.editor.commands.first(({ commands }) => [ () => commands.undoInputRule(), + // maybe convert first text block node to default node () => commands.command(({ tr }) => { const { selection, doc } = tr @@ -32,6 +33,7 @@ export const Keymap = Extension.create({ return commands.clearNodes() }), + () => commands.deleteSelection(), () => commands.joinBackward(), () => commands.selectNodeBackward(), diff --git a/packages/core/src/helpers/index.ts b/packages/core/src/helpers/index.ts index d46261939ad..69917e8ef59 100644 --- a/packages/core/src/helpers/index.ts +++ b/packages/core/src/helpers/index.ts @@ -44,6 +44,7 @@ export * from './isNodeActive.js' export * from './isNodeEmpty.js' export * from './isNodeSelection.js' export * from './isTextSelection.js' +export * from './listHelpers/index.js' export * from './posToDOMRect.js' export * from './resolveFocusPosition.js' export * from './selectionToInsertionEnd.js' diff --git a/packages/extension-list-item/src/helpers/getCurrentListItemPos.ts b/packages/core/src/helpers/listHelpers/findListItemPos.ts similarity index 67% rename from packages/extension-list-item/src/helpers/getCurrentListItemPos.ts rename to packages/core/src/helpers/listHelpers/findListItemPos.ts index b2a854628d5..370ec73959d 100644 --- a/packages/extension-list-item/src/helpers/getCurrentListItemPos.ts +++ b/packages/core/src/helpers/listHelpers/findListItemPos.ts @@ -1,8 +1,9 @@ -import { getNodeType } from '@tiptap/core' +import { NodeType } from '@tiptap/pm/model' import { EditorState } from '@tiptap/pm/state' -import { ResolvedPos } from 'prosemirror-model' -export const getCurrentListItemPos = (typeOrName: string, state: EditorState): ResolvedPos | undefined => { +import { getNodeType } from '../getNodeType.js' + +export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => { const { $from } = state.selection const nodeType = getNodeType(typeOrName, state.schema) @@ -23,8 +24,8 @@ export const getCurrentListItemPos = (typeOrName: string, state: EditorState): R } if (targetDepth === null) { - return + return null } - return state.doc.resolve(currentPos) + return { $pos: state.doc.resolve(currentPos), depth: targetDepth } } diff --git a/packages/core/src/helpers/listHelpers/getNextListDepth.ts b/packages/core/src/helpers/listHelpers/getNextListDepth.ts new file mode 100644 index 00000000000..845d657431d --- /dev/null +++ b/packages/core/src/helpers/listHelpers/getNextListDepth.ts @@ -0,0 +1,16 @@ +import { EditorState } from '@tiptap/pm/state' + +import { getNodeAtPosition } from '../getNodeAtPosition.js' +import { findListItemPos } from './findListItemPos.js' + +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 +} diff --git a/packages/extension-list-item/src/handlers/handleBackspace.ts b/packages/core/src/helpers/listHelpers/handleBackspace.ts similarity index 83% rename from packages/extension-list-item/src/handlers/handleBackspace.ts rename to packages/core/src/helpers/listHelpers/handleBackspace.ts index 7ea95226f22..c60c1826876 100644 --- a/packages/extension-list-item/src/handlers/handleBackspace.ts +++ b/packages/core/src/helpers/listHelpers/handleBackspace.ts @@ -1,10 +1,12 @@ -import { Editor, isAtStartOfNode, isNodeActive } from '@tiptap/core' import { Node } from '@tiptap/pm/model' -import { hasListBefore } from '../helpers/hasListBefore.js' -import { - findListItemPos, hasListItemBefore, listItemHasSubList, -} from '../helpers/index.js' +import { Editor } from '../../Editor.js' +import { isAtStartOfNode } from '../isAtStartOfNode.js' +import { isNodeActive } from '../isNodeActive.js' +import { findListItemPos } from './findListItemPos.js' +import { hasListBefore } from './hasListBefore.js' +import { hasListItemBefore } from './hasListItemBefore.js' +import { listItemHasSubList } from './listItemHasSubList.js' export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => { // this is required to still handle the undo handling @@ -64,7 +66,7 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s // if the previous item is a list item and doesn't have a sublist, join the list items if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) { - return editor.commands.joinListItemBackward(name) + return editor.commands.joinItemBackward() } // otherwise in the end, a backspace should diff --git a/packages/extension-list-item/src/handlers/handleDelete.ts b/packages/core/src/helpers/listHelpers/handleDelete.ts similarity index 70% rename from packages/extension-list-item/src/handlers/handleDelete.ts rename to packages/core/src/helpers/listHelpers/handleDelete.ts index a395b851ff9..16c3f3532fb 100644 --- a/packages/extension-list-item/src/handlers/handleDelete.ts +++ b/packages/core/src/helpers/listHelpers/handleDelete.ts @@ -1,6 +1,8 @@ -import { Editor, isAtEndOfNode, isNodeActive } from '@tiptap/core' - -import { nextListIsDeeper, nextListIsHigher } from '../helpers/index.js' +import { Editor } from '../../Editor.js' +import { isAtEndOfNode } from '../isAtEndOfNode.js' +import { isNodeActive } from '../isNodeActive.js' +import { nextListIsDeeper } from './nextListIsDeeper.js' +import { nextListIsHigher } from './nextListIsHigher.js' export const handleDelete = (editor: Editor, name: string) => { // if the cursor is not inside the current node type @@ -32,6 +34,5 @@ export const handleDelete = (editor: Editor, name: string) => { .run() } - // check if the next node is also a listItem - return editor.commands.joinListItemForward(name) + return editor.commands.joinItemForward() } diff --git a/packages/extension-list-item/src/helpers/hasListBefore.ts b/packages/core/src/helpers/listHelpers/hasListBefore.ts similarity index 100% rename from packages/extension-list-item/src/helpers/hasListBefore.ts rename to packages/core/src/helpers/listHelpers/hasListBefore.ts diff --git a/packages/extension-list-item/src/helpers/hasListItemAfter.ts b/packages/core/src/helpers/listHelpers/hasListItemAfter.ts similarity index 100% rename from packages/extension-list-item/src/helpers/hasListItemAfter.ts rename to packages/core/src/helpers/listHelpers/hasListItemAfter.ts diff --git a/packages/extension-list-item/src/helpers/hasListItemBefore.ts b/packages/core/src/helpers/listHelpers/hasListItemBefore.ts similarity index 100% rename from packages/extension-list-item/src/helpers/hasListItemBefore.ts rename to packages/core/src/helpers/listHelpers/hasListItemBefore.ts diff --git a/packages/core/src/helpers/listHelpers/index.ts b/packages/core/src/helpers/listHelpers/index.ts new file mode 100644 index 00000000000..0b95e720953 --- /dev/null +++ b/packages/core/src/helpers/listHelpers/index.ts @@ -0,0 +1,10 @@ +export * from './findListItemPos.js' +export * from './getNextListDepth.js' +export * from './handleBackspace.js' +export * from './handleDelete.js' +export * from './hasListBefore.js' +export * from './hasListItemAfter.js' +export * from './hasListItemBefore.js' +export * from './listItemHasSubList.js' +export * from './nextListIsDeeper.js' +export * from './nextListIsHigher.js' diff --git a/packages/extension-list-item/src/helpers/listItemhasSublist.ts b/packages/core/src/helpers/listHelpers/listItemHasSubList.ts similarity index 89% rename from packages/extension-list-item/src/helpers/listItemhasSublist.ts rename to packages/core/src/helpers/listHelpers/listItemHasSubList.ts index 78c542a7543..462b96db008 100644 --- a/packages/extension-list-item/src/helpers/listItemhasSublist.ts +++ b/packages/core/src/helpers/listHelpers/listItemHasSubList.ts @@ -1,7 +1,8 @@ -import { getNodeType } from '@tiptap/core' import { Node } from '@tiptap/pm/model' import { EditorState } from '@tiptap/pm/state' +import { getNodeType } from '../getNodeType.js' + export const listItemHasSubList = (typeOrName: string, state: EditorState, node?: Node) => { if (!node) { return false diff --git a/packages/core/src/helpers/listHelpers/nextListIsDeeper.ts b/packages/core/src/helpers/listHelpers/nextListIsDeeper.ts new file mode 100644 index 00000000000..5a133d15059 --- /dev/null +++ b/packages/core/src/helpers/listHelpers/nextListIsDeeper.ts @@ -0,0 +1,19 @@ +import { EditorState } from '@tiptap/pm/state' + +import { findListItemPos } from './findListItemPos.js' +import { getNextListDepth } from './getNextListDepth.js' + +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 +} diff --git a/packages/core/src/helpers/listHelpers/nextListIsHigher.ts b/packages/core/src/helpers/listHelpers/nextListIsHigher.ts new file mode 100644 index 00000000000..3e7ea112109 --- /dev/null +++ b/packages/core/src/helpers/listHelpers/nextListIsHigher.ts @@ -0,0 +1,19 @@ +import { EditorState } from '@tiptap/pm/state' + +import { findListItemPos } from './findListItemPos.js' +import { getNextListDepth } from './getNextListDepth.js' + +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 +} diff --git a/packages/extension-list-item/src/helpers/index.ts b/packages/extension-list-item/src/helpers/index.ts deleted file mode 100644 index c0e28ba2004..00000000000 --- a/packages/extension-list-item/src/helpers/index.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { getNodeAtPosition, getNodeType } from '@tiptap/core' -import { NodeType } from '@tiptap/pm/model' -import { EditorState } from '@tiptap/pm/state' - -export * from './hasListItemAfter.js' -export * from './hasListItemBefore.js' -export * from './listItemhasSublist.js' - -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 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 -} diff --git a/packages/extension-list-item/src/list-item.ts b/packages/extension-list-item/src/list-item.ts index 8515751f677..1fbd8e4f04f 100644 --- a/packages/extension-list-item/src/list-item.ts +++ b/packages/extension-list-item/src/list-item.ts @@ -1,24 +1,7 @@ import { + handleBackspace, handleDelete, mergeAttributes, Node, } from '@tiptap/core' -import { NodeType } from '@tiptap/pm/model' - -import { joinListItemBackward } from './commands/joinListItemBackward.js' -import { joinListItemForward } from './commands/joinListItemForward.js' -import { handleBackspace } from './handlers/handleBackspace.js' -import { handleDelete } from './handlers/handleDelete.js' - -declare module '@tiptap/core' { - interface Commands { - listItem: { - /** - * Lift the list item into a wrapping list. - */ - joinListItemForward: (typeOrName: string | NodeType) => ReturnType - joinListItemBackward: (typeOrName: string | NodeType) => ReturnType - } - } -} export interface ListItemOptions { HTMLAttributes: Record, @@ -53,13 +36,6 @@ export const ListItem = Node.create({ return ['li', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes), 0] }, - addCommands() { - return { - joinListItemForward, - joinListItemBackward, - } - }, - addKeyboardShortcuts() { return { Enter: () => this.editor.commands.splitListItem(this.name), diff --git a/packages/extension-task-item/src/task-item.ts b/packages/extension-task-item/src/task-item.ts index b365117cf19..f45c4c1e298 100644 --- a/packages/extension-task-item/src/task-item.ts +++ b/packages/extension-task-item/src/task-item.ts @@ -1,10 +1,13 @@ -import { mergeAttributes, Node, wrappingInputRule } from '@tiptap/core' +import { + handleBackspace, handleDelete, KeyboardShortcutCommand, mergeAttributes, Node, wrappingInputRule, +} from '@tiptap/core' import { Node as ProseMirrorNode } from '@tiptap/pm/model' export interface TaskItemOptions { onReadOnlyChecked?: (node: ProseMirrorNode, checked: boolean) => boolean nested: boolean HTMLAttributes: Record + taskListTypeName: string } export const inputRegex = /^\s*(\[([( |x])?\])\s$/ @@ -16,6 +19,7 @@ export const TaskItem = Node.create({ return { nested: false, HTMLAttributes: {}, + taskListTypeName: 'taskList', } }, @@ -69,9 +73,15 @@ export const TaskItem = Node.create({ }, addKeyboardShortcuts() { - const shortcuts = { + const shortcuts: { + [key: string]: KeyboardShortcutCommand + } = { Enter: () => this.editor.commands.splitListItem(this.name), 'Shift-Tab': () => this.editor.commands.liftListItem(this.name), + Delete: ({ editor }) => handleDelete(editor, this.name), + 'Mod-Delete': ({ editor }) => handleDelete(editor, this.name), + Backspace: ({ editor }) => handleBackspace(editor, this.name, [this.options.taskListTypeName]), + 'Mod-Backspace': ({ editor }) => handleBackspace(editor, this.name, [this.options.taskListTypeName]), } if (!this.options.nested) { From 2a11d39fe90f1e5a1e251414f262a148a33b92ef Mon Sep 17 00:00:00 2001 From: bdbch Date: Thu, 13 Jul 2023 15:38:35 +0200 Subject: [PATCH 4/4] add option to check for node in isAtEndOfNode --- packages/core/src/helpers/isAtEndOfNode.ts | 22 +++++++++++++++++-- .../src/helpers/listHelpers/handleDelete.ts | 2 +- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/packages/core/src/helpers/isAtEndOfNode.ts b/packages/core/src/helpers/isAtEndOfNode.ts index 9ea038efe6e..0581ec024f4 100644 --- a/packages/core/src/helpers/isAtEndOfNode.ts +++ b/packages/core/src/helpers/isAtEndOfNode.ts @@ -1,7 +1,25 @@ import { EditorState } from '@tiptap/pm/state' -export const isAtEndOfNode = (state: EditorState) => { - const { $from, $to } = state.selection +import { findParentNode } from './findParentNode.js' + +export const isAtEndOfNode = (state: EditorState, nodeType?: string) => { + const { $from, $to, $anchor } = state.selection + + if (nodeType) { + const parentNode = findParentNode(node => node.type.name === nodeType)(state.selection) + + if (!parentNode) { + return false + } + + const $parentPos = state.doc.resolve(parentNode.pos + 1) + + if ($anchor.pos + 1 === $parentPos.end()) { + return true + } + + return false + } if ($to.parentOffset < $to.parent.nodeSize - 2 || $from.pos !== $to.pos) { return false diff --git a/packages/core/src/helpers/listHelpers/handleDelete.ts b/packages/core/src/helpers/listHelpers/handleDelete.ts index 16c3f3532fb..bebaa145121 100644 --- a/packages/core/src/helpers/listHelpers/handleDelete.ts +++ b/packages/core/src/helpers/listHelpers/handleDelete.ts @@ -13,7 +13,7 @@ export const handleDelete = (editor: Editor, name: string) => { // if the cursor is not at the end of a node // do nothing and proceed - if (!isAtEndOfNode(editor.state)) { + if (!isAtEndOfNode(editor.state, name)) { return false }