@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.
- package/dist/index.cjs +192 -68
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +170 -46
- package/dist/index.js.map +1 -1
- package/dist/item/index.cjs +120 -3
- package/dist/item/index.cjs.map +1 -1
- package/dist/item/index.js +117 -0
- package/dist/item/index.js.map +1 -1
- package/dist/keymap/index.cjs +37 -47
- package/dist/keymap/index.cjs.map +1 -1
- package/dist/keymap/index.js +30 -40
- package/dist/keymap/index.js.map +1 -1
- package/dist/kit/index.cjs +192 -68
- package/dist/kit/index.cjs.map +1 -1
- package/dist/kit/index.js +170 -46
- package/dist/kit/index.js.map +1 -1
- package/dist/ordered-list/index.cjs +8 -1
- package/dist/ordered-list/index.cjs.map +1 -1
- package/dist/ordered-list/index.js +8 -1
- package/dist/ordered-list/index.js.map +1 -1
- package/dist/task-item/index.cjs +120 -5
- package/dist/task-item/index.cjs.map +1 -1
- package/dist/task-item/index.js +115 -0
- package/dist/task-item/index.js.map +1 -1
- package/dist/task-list/index.cjs +5 -1
- package/dist/task-list/index.cjs.map +1 -1
- package/dist/task-list/index.js +5 -1
- package/dist/task-list/index.js.map +1 -1
- package/package.json +19 -20
- package/src/helpers/createBranchingListDeleteKeymap.ts +24 -0
- package/src/helpers/getBranchingNestedListAtCursor.ts +116 -0
- package/src/helpers/handleDeleteBranchingNestedList.ts +25 -0
- package/src/helpers/hasBranchingNestedListAfterCursor.ts +30 -0
- package/src/helpers/hoistBranchingNestedList.ts +56 -0
- package/src/item/list-item.ts +21 -5
- package/src/keymap/listHelpers/handleBackspace.ts +3 -22
- package/src/keymap/listHelpers/hasListBefore.ts +5 -1
- package/src/ordered-list/ordered-list.ts +3 -1
- package/src/ordered-list/utils.ts +15 -2
- package/src/task-item/task-item.ts +10 -0
- package/src/task-list/task-list.ts +5 -1
package/dist/task-list/index.js
CHANGED
|
@@ -21,7 +21,11 @@ var TaskList = Node.create({
|
|
|
21
21
|
];
|
|
22
22
|
},
|
|
23
23
|
renderHTML({ HTMLAttributes }) {
|
|
24
|
-
return [
|
|
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'
|
|
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/
|
|
91
|
-
"@tiptap/
|
|
95
|
+
"@tiptap/core": "^3.25.0",
|
|
96
|
+
"@tiptap/pm": "^3.25.0"
|
|
92
97
|
},
|
|
93
98
|
"peerDependencies": {
|
|
94
|
-
"@tiptap/core": "3.
|
|
95
|
-
"@tiptap/pm": "3.
|
|
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
|
+
}
|
package/src/item/list-item.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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 (
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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 = (
|
|
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')
|
|
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 {
|
|
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(
|
|
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 [
|
|
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) => {
|