@tiptap/extension-list 3.23.6 → 3.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/index.cjs +192 -68
  2. package/dist/index.cjs.map +1 -1
  3. package/dist/index.js +170 -46
  4. package/dist/index.js.map +1 -1
  5. package/dist/item/index.cjs +120 -3
  6. package/dist/item/index.cjs.map +1 -1
  7. package/dist/item/index.js +117 -0
  8. package/dist/item/index.js.map +1 -1
  9. package/dist/keymap/index.cjs +37 -47
  10. package/dist/keymap/index.cjs.map +1 -1
  11. package/dist/keymap/index.js +30 -40
  12. package/dist/keymap/index.js.map +1 -1
  13. package/dist/kit/index.cjs +192 -68
  14. package/dist/kit/index.cjs.map +1 -1
  15. package/dist/kit/index.js +170 -46
  16. package/dist/kit/index.js.map +1 -1
  17. package/dist/ordered-list/index.cjs +8 -1
  18. package/dist/ordered-list/index.cjs.map +1 -1
  19. package/dist/ordered-list/index.js +8 -1
  20. package/dist/ordered-list/index.js.map +1 -1
  21. package/dist/task-item/index.cjs +120 -5
  22. package/dist/task-item/index.cjs.map +1 -1
  23. package/dist/task-item/index.js +115 -0
  24. package/dist/task-item/index.js.map +1 -1
  25. package/dist/task-list/index.cjs +5 -1
  26. package/dist/task-list/index.cjs.map +1 -1
  27. package/dist/task-list/index.js +5 -1
  28. package/dist/task-list/index.js.map +1 -1
  29. package/package.json +19 -20
  30. package/src/helpers/createBranchingListDeleteKeymap.ts +24 -0
  31. package/src/helpers/getBranchingNestedListAtCursor.ts +116 -0
  32. package/src/helpers/handleDeleteBranchingNestedList.ts +25 -0
  33. package/src/helpers/hasBranchingNestedListAfterCursor.ts +30 -0
  34. package/src/helpers/hoistBranchingNestedList.ts +56 -0
  35. package/src/item/list-item.ts +21 -5
  36. package/src/keymap/listHelpers/handleBackspace.ts +3 -22
  37. package/src/keymap/listHelpers/hasListBefore.ts +5 -1
  38. package/src/ordered-list/ordered-list.ts +3 -1
  39. package/src/ordered-list/utils.ts +15 -2
  40. package/src/task-item/task-item.ts +10 -0
  41. package/src/task-list/task-list.ts +5 -1
@@ -21,7 +21,11 @@ var TaskList = Node.create({
21
21
  ];
22
22
  },
23
23
  renderHTML({ HTMLAttributes }) {
24
- return ["ul", mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }), 0];
24
+ return [
25
+ "ul",
26
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { "data-type": this.name }),
27
+ 0
28
+ ];
25
29
  },
26
30
  parseMarkdown: (token, h) => {
27
31
  return h.createNode("taskList", {}, h.parseChildren(token.items || []));
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/task-list/task-list.ts"],"sourcesContent":["import { mergeAttributes, Node, parseIndentedBlocks } from '@tiptap/core'\n\nexport interface TaskListOptions {\n /**\n * The node type name for a task item.\n * @default 'taskItem'\n * @example 'myCustomTaskItem'\n */\n itemTypeName: string\n\n /**\n * The HTML attributes for a task list node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n taskList: {\n /**\n * Toggle a task list\n * @example editor.commands.toggleTaskList()\n */\n toggleTaskList: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to create task lists.\n * @see https://www.tiptap.dev/api/nodes/task-list\n */\nexport const TaskList = Node.create<TaskListOptions>({\n name: 'taskList',\n\n addOptions() {\n return {\n itemTypeName: 'taskItem',\n HTMLAttributes: {},\n }\n },\n\n group: 'block list',\n\n content() {\n return `${this.options.itemTypeName}+`\n },\n\n parseHTML() {\n return [\n {\n tag: `ul[data-type=\"${this.name}\"]`,\n priority: 51,\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }), 0]\n },\n\n parseMarkdown: (token, h) => {\n return h.createNode('taskList', {}, h.parseChildren(token.items || []))\n },\n\n renderMarkdown: (node, h) => {\n if (!node.content) {\n return ''\n }\n\n return h.renderChildren(node.content, '\\n')\n },\n\n markdownTokenizer: {\n name: 'taskList',\n level: 'block',\n start(src) {\n // Look for the start of a task list item\n const index = src.match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+/)?.index\n return index !== undefined ? index : -1\n },\n tokenize(src, tokens, lexer) {\n // Helper function to recursively parse task lists\n const parseTaskListContent = (content: string): any[] | undefined => {\n const nestedResult = parseIndentedBlocks(\n content,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Allow recursive nesting\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (nestedResult) {\n // Return as task list token\n return [\n {\n type: 'taskList',\n raw: nestedResult.raw,\n items: nestedResult.items,\n },\n ]\n }\n\n // Fall back to regular markdown parsing if not a task list\n return lexer.blockTokens(content)\n }\n\n const result = parseIndentedBlocks(\n src,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Use the recursive parser for nested content\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (!result) {\n return undefined\n }\n\n return {\n type: 'taskList',\n raw: result.raw,\n items: result.items,\n }\n },\n },\n\n markdownOptions: {\n indentsContent: true,\n },\n\n addCommands() {\n return {\n toggleTaskList:\n () =>\n ({ commands }) => {\n return commands.toggleList(this.name, this.options.itemTypeName)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-Shift-9': () => this.editor.commands.toggleTaskList(),\n }\n },\n})\n"],"mappings":"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","names":[]}
1
+ {"version":3,"sources":["../../src/task-list/task-list.ts"],"sourcesContent":["import { mergeAttributes, Node, parseIndentedBlocks } from '@tiptap/core'\n\nexport interface TaskListOptions {\n /**\n * The node type name for a task item.\n * @default 'taskItem'\n * @example 'myCustomTaskItem'\n */\n itemTypeName: string\n\n /**\n * The HTML attributes for a task list node.\n * @default {}\n * @example { class: 'foo' }\n */\n HTMLAttributes: Record<string, any>\n}\n\ndeclare module '@tiptap/core' {\n interface Commands<ReturnType> {\n taskList: {\n /**\n * Toggle a task list\n * @example editor.commands.toggleTaskList()\n */\n toggleTaskList: () => ReturnType\n }\n }\n}\n\n/**\n * This extension allows you to create task lists.\n * @see https://www.tiptap.dev/api/nodes/task-list\n */\nexport const TaskList = Node.create<TaskListOptions>({\n name: 'taskList',\n\n addOptions() {\n return {\n itemTypeName: 'taskItem',\n HTMLAttributes: {},\n }\n },\n\n group: 'block list',\n\n content() {\n return `${this.options.itemTypeName}+`\n },\n\n parseHTML() {\n return [\n {\n tag: `ul[data-type=\"${this.name}\"]`,\n priority: 51,\n },\n ]\n },\n\n renderHTML({ HTMLAttributes }) {\n return [\n 'ul',\n mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }),\n 0,\n ]\n },\n\n parseMarkdown: (token, h) => {\n return h.createNode('taskList', {}, h.parseChildren(token.items || []))\n },\n\n renderMarkdown: (node, h) => {\n if (!node.content) {\n return ''\n }\n\n return h.renderChildren(node.content, '\\n')\n },\n\n markdownTokenizer: {\n name: 'taskList',\n level: 'block',\n start(src) {\n // Look for the start of a task list item\n const index = src.match(/^\\s*[-+*]\\s+\\[([ xX])\\]\\s+/)?.index\n return index !== undefined ? index : -1\n },\n tokenize(src, tokens, lexer) {\n // Helper function to recursively parse task lists\n const parseTaskListContent = (content: string): any[] | undefined => {\n const nestedResult = parseIndentedBlocks(\n content,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Allow recursive nesting\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (nestedResult) {\n // Return as task list token\n return [\n {\n type: 'taskList',\n raw: nestedResult.raw,\n items: nestedResult.items,\n },\n ]\n }\n\n // Fall back to regular markdown parsing if not a task list\n return lexer.blockTokens(content)\n }\n\n const result = parseIndentedBlocks(\n src,\n {\n itemPattern: /^(\\s*)([-+*])\\s+\\[([ xX])\\]\\s+(.*)$/,\n extractItemData: match => ({\n indentLevel: match[1].length,\n mainContent: match[4],\n checked: match[3].toLowerCase() === 'x',\n }),\n createToken: (data, nestedTokens) => ({\n type: 'taskItem',\n raw: '',\n mainContent: data.mainContent,\n indentLevel: data.indentLevel,\n checked: data.checked,\n text: data.mainContent,\n tokens: lexer.inlineTokens(data.mainContent),\n nestedTokens,\n }),\n // Use the recursive parser for nested content\n customNestedParser: parseTaskListContent,\n },\n lexer,\n )\n\n if (!result) {\n return undefined\n }\n\n return {\n type: 'taskList',\n raw: result.raw,\n items: result.items,\n }\n },\n },\n\n markdownOptions: {\n indentsContent: true,\n },\n\n addCommands() {\n return {\n toggleTaskList:\n () =>\n ({ commands }) => {\n return commands.toggleList(this.name, this.options.itemTypeName)\n },\n }\n },\n\n addKeyboardShortcuts() {\n return {\n 'Mod-Shift-9': () => this.editor.commands.toggleTaskList(),\n }\n },\n})\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,18 +1,30 @@
1
1
  {
2
2
  "name": "@tiptap/extension-list",
3
+ "version": "3.25.0",
3
4
  "description": "List extension for tiptap",
4
- "version": "3.23.6",
5
- "homepage": "https://tiptap.dev",
6
5
  "keywords": [
7
6
  "tiptap",
8
7
  "tiptap extension"
9
8
  ],
9
+ "homepage": "https://tiptap.dev",
10
10
  "license": "MIT",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/ueberdosis/tiptap",
14
+ "directory": "packages/extension-list"
15
+ },
11
16
  "funding": {
12
17
  "type": "github",
13
18
  "url": "https://github.com/sponsors/ueberdosis"
14
19
  },
20
+ "files": [
21
+ "src",
22
+ "dist"
23
+ ],
15
24
  "type": "module",
25
+ "main": "dist/index.cjs",
26
+ "module": "dist/index.js",
27
+ "types": "dist/index.d.ts",
16
28
  "exports": {
17
29
  ".": {
18
30
  "types": {
@@ -79,28 +91,15 @@
79
91
  "require": "./dist/task-list/index.cjs"
80
92
  }
81
93
  },
82
- "main": "dist/index.cjs",
83
- "module": "dist/index.js",
84
- "types": "dist/index.d.ts",
85
- "files": [
86
- "src",
87
- "dist"
88
- ],
89
94
  "devDependencies": {
90
- "@tiptap/pm": "^3.23.6",
91
- "@tiptap/core": "^3.23.6"
95
+ "@tiptap/core": "^3.25.0",
96
+ "@tiptap/pm": "^3.25.0"
92
97
  },
93
98
  "peerDependencies": {
94
- "@tiptap/core": "3.23.6",
95
- "@tiptap/pm": "3.23.6"
96
- },
97
- "repository": {
98
- "type": "git",
99
- "url": "https://github.com/ueberdosis/tiptap",
100
- "directory": "packages/extension-list"
99
+ "@tiptap/core": "3.25.0",
100
+ "@tiptap/pm": "3.25.0"
101
101
  },
102
102
  "scripts": {
103
- "build": "tsup",
104
- "lint": "prettier ./src/ --check && eslint --cache --quiet --no-error-on-unmatched-pattern ./src/"
103
+ "build": "tsup"
105
104
  }
106
105
  }
@@ -0,0 +1,24 @@
1
+ import { Extension } from '@tiptap/core'
2
+
3
+ import { handleDeleteBranchingNestedList } from './handleDeleteBranchingNestedList.js'
4
+
5
+ /**
6
+ * Creates a high-priority keymap extension that handles Delete for branching nested lists.
7
+ * Kept separate from the list item node so Enter/Tab shortcuts keep their default priority.
8
+ */
9
+ export const createBranchingListDeleteKeymap = (itemName: string, wrapperNames: string[]) => {
10
+ return Extension.create({
11
+ name: `${itemName}BranchingDeleteKeymap`,
12
+ priority: 101,
13
+
14
+ addKeyboardShortcuts() {
15
+ const handleDelete = () =>
16
+ handleDeleteBranchingNestedList(this.editor, itemName, wrapperNames)
17
+
18
+ return {
19
+ Delete: handleDelete,
20
+ 'Mod-Delete': handleDelete,
21
+ }
22
+ },
23
+ })
24
+ }
@@ -0,0 +1,116 @@
1
+ import type { Node } from '@tiptap/pm/model'
2
+ import type { EditorState } from '@tiptap/pm/state'
3
+
4
+ export type BranchingNestedListAtCursor = {
5
+ listItemDepth: number
6
+ nestedList: Node
7
+ nestedListPos: number
8
+ insertPos: number
9
+ items: Node[]
10
+ }
11
+
12
+ /**
13
+ * Resolves a branching nested list immediately after the cursor when the selection is
14
+ * collapsed at the end of a textblock inside a list item.
15
+ *
16
+ * @param state - The editor state to inspect.
17
+ * @param itemName - The list item node name (for example `listItem` or `taskItem`).
18
+ * @param wrapperNames - List wrapper node names (for example `bulletList` and `orderedList`).
19
+ * @returns Resolved positions and nodes for hoisting, or `null` when not applicable.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const context = getBranchingNestedListAtCursor(editor.state, 'listItem', [
24
+ * 'bulletList',
25
+ * 'orderedList',
26
+ * ])
27
+ *
28
+ * if (context) {
29
+ * // cursor is at the end of Item 1 before a branching nested sublist
30
+ * }
31
+ * ```
32
+ */
33
+ export const getBranchingNestedListAtCursor = (
34
+ state: EditorState,
35
+ itemName: string,
36
+ wrapperNames: string[],
37
+ ): BranchingNestedListAtCursor | null => {
38
+ const { selection } = state
39
+
40
+ if (!selection.empty) {
41
+ return null
42
+ }
43
+
44
+ const { $from } = selection
45
+
46
+ if (!$from.parent.isTextblock) {
47
+ return null
48
+ }
49
+
50
+ if ($from.parentOffset !== $from.parent.content.size) {
51
+ return null
52
+ }
53
+
54
+ let listItemDepth = -1
55
+
56
+ for (let depth = $from.depth; depth > 0; depth -= 1) {
57
+ if ($from.node(depth).type.name === itemName) {
58
+ listItemDepth = depth
59
+ break
60
+ }
61
+ }
62
+
63
+ if (listItemDepth < 0) {
64
+ return null
65
+ }
66
+
67
+ const listItem = $from.node(listItemDepth)
68
+ const indexInListItem = $from.index(listItemDepth)
69
+
70
+ if (indexInListItem + 1 >= listItem.childCount) {
71
+ return null
72
+ }
73
+
74
+ const nextChild = listItem.child(indexInListItem + 1)
75
+
76
+ if (!wrapperNames.includes(nextChild.type.name)) {
77
+ return null
78
+ }
79
+
80
+ const itemType = state.schema.nodes[itemName]
81
+ let hasBranching = false
82
+
83
+ nextChild.forEach(child => {
84
+ if (child.type === itemType && child.childCount > 1) {
85
+ hasBranching = true
86
+ }
87
+ })
88
+
89
+ if (!hasBranching) {
90
+ return null
91
+ }
92
+
93
+ const nodeAfter = state.doc.resolve($from.after()).nodeAfter
94
+
95
+ if (!nodeAfter || !wrapperNames.includes(nodeAfter.type.name)) {
96
+ return null
97
+ }
98
+
99
+ const items: Node[] = []
100
+
101
+ nodeAfter.forEach(child => {
102
+ items.push(child)
103
+ })
104
+
105
+ if (items.length === 0) {
106
+ return null
107
+ }
108
+
109
+ return {
110
+ listItemDepth,
111
+ nestedList: nodeAfter,
112
+ nestedListPos: $from.after(),
113
+ insertPos: $from.after(listItemDepth),
114
+ items,
115
+ }
116
+ }
@@ -0,0 +1,25 @@
1
+ import type { Editor } from '@tiptap/core'
2
+
3
+ import { hoistBranchingNestedList } from './hoistBranchingNestedList.js'
4
+
5
+ /**
6
+ * Handles Delete for a list item when a branching nested sublist follows the cursor.
7
+ *
8
+ * @param editor - The editor instance whose state should be updated.
9
+ * @param itemName - The list item node name (for example `listItem` or `taskItem`).
10
+ * @param wrapperNames - List wrapper node names (for example `bulletList` and `orderedList`).
11
+ * @returns `true` when the nested list was hoisted, otherwise `false`.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * Delete: () =>
16
+ * handleDeleteBranchingNestedList(editor, 'listItem', ['bulletList', 'orderedList']),
17
+ * ```
18
+ */
19
+ export const handleDeleteBranchingNestedList = (
20
+ editor: Editor,
21
+ itemName: string,
22
+ wrapperNames: string[],
23
+ ) => {
24
+ return hoistBranchingNestedList(editor.state, editor.view.dispatch, itemName, wrapperNames)
25
+ }
@@ -0,0 +1,30 @@
1
+ import type { EditorState } from '@tiptap/pm/state'
2
+
3
+ import { getBranchingNestedListAtCursor } from './getBranchingNestedListAtCursor.js'
4
+
5
+ /**
6
+ * Returns whether the cursor is at the end of a list item textblock and the next
7
+ * sibling is a nested list that contains at least one branching list item.
8
+ *
9
+ * @param state - The editor state to inspect.
10
+ * @param itemName - The list item node name (for example `listItem` or `taskItem`).
11
+ * @param wrapperNames - List wrapper node names (for example `bulletList` and `orderedList`).
12
+ * @returns `true` when {@link hoistBranchingNestedList} should handle Delete.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * if (hasBranchingNestedListAfterCursor(editor.state, 'listItem', ['bulletList', 'orderedList'])) {
17
+ * hoistBranchingNestedList(editor.state, editor.view.dispatch, 'listItem', [
18
+ * 'bulletList',
19
+ * 'orderedList',
20
+ * ])
21
+ * }
22
+ * ```
23
+ */
24
+ export const hasBranchingNestedListAfterCursor = (
25
+ state: EditorState,
26
+ itemName: string,
27
+ wrapperNames: string[],
28
+ ): boolean => {
29
+ return getBranchingNestedListAtCursor(state, itemName, wrapperNames) !== null
30
+ }
@@ -0,0 +1,56 @@
1
+ import { Fragment } from '@tiptap/pm/model'
2
+ import type { EditorState, Transaction } from '@tiptap/pm/state'
3
+
4
+ import { getBranchingNestedListAtCursor } from './getBranchingNestedListAtCursor.js'
5
+
6
+ /**
7
+ * Hoists all list items from a branching nested list after the cursor into the parent list.
8
+ *
9
+ * Use this when `joinForward` cannot restructure a nested list that contains list items
10
+ * with sublists (see issue #6906).
11
+ *
12
+ * @param state - The editor state to transform.
13
+ * @param dispatch - Optional dispatch function for the transaction.
14
+ * @param itemName - The list item node name (for example `listItem` or `taskItem`).
15
+ * @param wrapperNames - List wrapper node names (for example `bulletList` and `orderedList`).
16
+ * @returns `true` when the nested list was hoisted, otherwise `false`.
17
+ *
18
+ * @example
19
+ * ```ts
20
+ * // Cursor at the end of "Item 1" before a nested list with branching items.
21
+ * hoistBranchingNestedList(editor.state, editor.view.dispatch, 'listItem', [
22
+ * 'bulletList',
23
+ * 'orderedList',
24
+ * ])
25
+ * ```
26
+ */
27
+ export const hoistBranchingNestedList = (
28
+ state: EditorState,
29
+ dispatch: ((tr: Transaction) => void) | undefined,
30
+ itemName: string,
31
+ wrapperNames: string[],
32
+ ) => {
33
+ const context = getBranchingNestedListAtCursor(state, itemName, wrapperNames)
34
+
35
+ if (!context) {
36
+ return false
37
+ }
38
+
39
+ const { selection } = state
40
+ const { nestedList, nestedListPos, insertPos, items } = context
41
+ const tr = state.tr
42
+
43
+ tr.delete(nestedListPos, nestedListPos + nestedList.nodeSize)
44
+
45
+ const mappedInsertPos = tr.mapping.map(insertPos)
46
+
47
+ tr.insert(mappedInsertPos, Fragment.from(items))
48
+
49
+ tr.setSelection(selection.map(tr.doc, tr.mapping))
50
+
51
+ if (dispatch) {
52
+ dispatch(tr)
53
+ }
54
+
55
+ return true
56
+ }
@@ -1,6 +1,8 @@
1
1
  import type { JSONContent, MarkdownParseHelpers, MarkdownToken } from '@tiptap/core'
2
2
  import { mergeAttributes, Node, renderNestedMarkdownContent } from '@tiptap/core'
3
3
 
4
+ import { createBranchingListDeleteKeymap } from '../helpers/createBranchingListDeleteKeymap.js'
5
+
4
6
  export interface ListItemOptions {
5
7
  /**
6
8
  * The HTML attributes for a list item node.
@@ -29,10 +31,10 @@ function isSameLineOrderedListToken(token: MarkdownToken): boolean {
29
31
 
30
32
  return Boolean(
31
33
  token.text &&
32
- token.tokens?.length === 1 &&
33
- nestedToken?.type === 'list' &&
34
- nestedToken.ordered &&
35
- nestedToken.raw === token.text,
34
+ token.tokens?.length === 1 &&
35
+ nestedToken?.type === 'list' &&
36
+ nestedToken.ordered &&
37
+ nestedToken.raw === token.text,
36
38
  )
37
39
  }
38
40
 
@@ -114,7 +116,12 @@ export const ListItem = Node.create<ListItemOptions>({
114
116
  // Check if the first token is a text token with nested inline tokens
115
117
  const firstToken = token.tokens[0]
116
118
 
117
- if (firstToken && firstToken.type === 'text' && firstToken.tokens && firstToken.tokens.length > 0) {
119
+ if (
120
+ firstToken &&
121
+ firstToken.type === 'text' &&
122
+ firstToken.tokens &&
123
+ firstToken.tokens.length > 0
124
+ ) {
118
125
  // Parse the inline content from the text token
119
126
  const inlineContent = helpers.parseInline(firstToken.tokens)
120
127
 
@@ -175,6 +182,15 @@ export const ListItem = Node.create<ListItemOptions>({
175
182
  )
176
183
  },
177
184
 
185
+ addExtensions() {
186
+ return [
187
+ createBranchingListDeleteKeymap(this.name, [
188
+ this.options.bulletListTypeName,
189
+ this.options.orderedListTypeName,
190
+ ]),
191
+ ]
192
+ },
193
+
178
194
  addKeyboardShortcuts() {
179
195
  return {
180
196
  Enter: () => this.editor.commands.splitListItem(this.name),
@@ -2,10 +2,7 @@ import type { Editor } from '@tiptap/core'
2
2
  import { isAtStartOfNode, isNodeActive } from '@tiptap/core'
3
3
  import type { Node } from '@tiptap/pm/model'
4
4
 
5
- import { findListItemPos } from './findListItemPos.js'
6
5
  import { hasListBefore } from './hasListBefore.js'
7
- import { hasListItemBefore } from './hasListItemBefore.js'
8
- import { listItemHasSubList } from './listItemHasSubList.js'
9
6
 
10
7
  export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
11
8
  // this is required to still handle the undo handling
@@ -62,24 +59,8 @@ export const handleBackspace = (editor: Editor, name: string, parentListTypes: s
62
59
  return false
63
60
  }
64
61
 
65
- const listItemPos = findListItemPos(name, editor.state)
66
-
67
- if (!listItemPos) {
68
- return false
69
- }
70
-
71
- const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2)
72
- const prevNode = $prev.node(listItemPos.depth)
73
-
74
- const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode)
75
-
76
- // if the previous item is a list item and doesn't have a sublist, join the list items
77
- if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) {
78
- return editor.commands.joinItemBackward()
79
- }
80
-
81
- // otherwise in the end, a backspace should
82
- // always just lift the list item if
83
- // joining / merging is not possible
62
+ // At the start of a list item, lift it out. Top-level items split the
63
+ // wrapping list around them; nested items get promoted into the outer
64
+ // list. A second backspace then falls through to the merge branch above.
84
65
  return editor.chain().liftListItem(name).run()
85
66
  }
@@ -1,6 +1,10 @@
1
1
  import type { EditorState } from '@tiptap/pm/state'
2
2
 
3
- export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
3
+ export const hasListBefore = (
4
+ editorState: EditorState,
5
+ name: string,
6
+ parentListTypes: string[],
7
+ ) => {
4
8
  const { $anchor } = editorState.selection
5
9
 
6
10
  const previousNodePos = Math.max(0, $anchor.pos - 2)
@@ -81,7 +81,9 @@ export const OrderedList = Node.create<OrderedListOptions>({
81
81
  start: {
82
82
  default: 1,
83
83
  parseHTML: element => {
84
- return element.hasAttribute('start') ? parseInt(element.getAttribute('start') || '', 10) : 1
84
+ return element.hasAttribute('start')
85
+ ? parseInt(element.getAttribute('start') || '', 10)
86
+ : 1
85
87
  },
86
88
  },
87
89
  type: {
@@ -1,4 +1,9 @@
1
- import type { JSONContent, MarkdownLexerConfiguration, MarkdownParseHelpers, MarkdownToken } from '@tiptap/core'
1
+ import type {
2
+ JSONContent,
3
+ MarkdownLexerConfiguration,
4
+ MarkdownParseHelpers,
5
+ MarkdownToken,
6
+ } from '@tiptap/core'
2
7
 
3
8
  /**
4
9
  * Matches an ordered list item line with optional leading whitespace.
@@ -28,10 +33,15 @@ function isBlockContentLine(line: string): boolean {
28
33
  const trimmedLine = line.trimStart()
29
34
 
30
35
  return (
36
+ // oxlint-disable-next-line prefer-string-starts-ends-with
31
37
  /^[-+*]\s+/.test(trimmedLine) ||
38
+ // oxlint-disable-next-line prefer-string-starts-ends-with
32
39
  /^\d+\.\s+/.test(trimmedLine) ||
40
+ // oxlint-disable-next-line prefer-string-starts-ends-with
33
41
  /^>\s?/.test(trimmedLine) ||
42
+ // oxlint-disable-next-line prefer-string-starts-ends-with
34
43
  /^```/.test(trimmedLine) ||
44
+ // oxlint-disable-next-line prefer-string-starts-ends-with
35
45
  /^~~~/.test(trimmedLine)
36
46
  )
37
47
  }
@@ -244,7 +254,10 @@ export function buildNestedStructure(
244
254
  * @param helpers - Markdown parse helpers for recursive parsing
245
255
  * @returns Array of listItem JSONContent nodes
246
256
  */
247
- export function parseListItems(items: MarkdownToken[], helpers: MarkdownParseHelpers): JSONContent[] {
257
+ export function parseListItems(
258
+ items: MarkdownToken[],
259
+ helpers: MarkdownParseHelpers,
260
+ ): JSONContent[] {
248
261
  return items.map(item => {
249
262
  if (item.type !== 'list_item') {
250
263
  return helpers.parseChildren([item])[0]
@@ -8,6 +8,8 @@ import {
8
8
  } from '@tiptap/core'
9
9
  import type { Node as ProseMirrorNode } from '@tiptap/pm/model'
10
10
 
11
+ import { createBranchingListDeleteKeymap } from '../helpers/createBranchingListDeleteKeymap.js'
12
+
11
13
  export interface TaskItemOptions {
12
14
  /**
13
15
  * A callback function that is called when the checkbox is clicked while the editor is in readonly mode.
@@ -158,6 +160,14 @@ export const TaskItem = Node.create<TaskItemOptions>({
158
160
  return renderNestedMarkdownContent(node, h, prefix)
159
161
  },
160
162
 
163
+ addExtensions() {
164
+ if (!this.options.nested) {
165
+ return []
166
+ }
167
+
168
+ return [createBranchingListDeleteKeymap(this.name, [this.options.taskListTypeName])]
169
+ },
170
+
161
171
  addKeyboardShortcuts() {
162
172
  const shortcuts: {
163
173
  [key: string]: KeyboardShortcutCommand
@@ -58,7 +58,11 @@ export const TaskList = Node.create<TaskListOptions>({
58
58
  },
59
59
 
60
60
  renderHTML({ HTMLAttributes }) {
61
- return ['ul', mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }), 0]
61
+ return [
62
+ 'ul',
63
+ mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, { 'data-type': this.name }),
64
+ 0,
65
+ ]
62
66
  },
63
67
 
64
68
  parseMarkdown: (token, h) => {