@tiptap/extension-list-keymap 2.1.0-rc.13

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 (43) hide show
  1. package/README.md +14 -0
  2. package/dist/index.cjs +416 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.js +410 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/index.umd.js +420 -0
  7. package/dist/index.umd.js.map +1 -0
  8. package/dist/packages/core/src/helpers/findParentNode.d.ts +8 -0
  9. package/dist/packages/core/src/helpers/findParentNodeClosestToPos.d.ts +8 -0
  10. package/dist/packages/core/src/helpers/getNodeAtPosition.d.ts +11 -0
  11. package/dist/packages/core/src/helpers/getNodeType.d.ts +2 -0
  12. package/dist/packages/core/src/helpers/isAtEndOfNode.d.ts +2 -0
  13. package/dist/packages/core/src/helpers/isAtStartOfNode.d.ts +2 -0
  14. package/dist/packages/core/src/helpers/isNodeActive.d.ts +3 -0
  15. package/dist/packages/core/src/utilities/isRegExp.d.ts +1 -0
  16. package/dist/packages/core/src/utilities/objectIncludes.d.ts +8 -0
  17. package/dist/packages/extension-list-keymap/src/index.d.ts +4 -0
  18. package/dist/packages/extension-list-keymap/src/list-keymap.d.ts +8 -0
  19. package/dist/packages/extension-list-keymap/src/listHelpers/findListItemPos.d.ts +6 -0
  20. package/dist/packages/extension-list-keymap/src/listHelpers/getNextListDepth.d.ts +2 -0
  21. package/dist/packages/extension-list-keymap/src/listHelpers/handleBackspace.d.ts +2 -0
  22. package/dist/packages/extension-list-keymap/src/listHelpers/handleDelete.d.ts +2 -0
  23. package/dist/packages/extension-list-keymap/src/listHelpers/hasListBefore.d.ts +2 -0
  24. package/dist/packages/extension-list-keymap/src/listHelpers/hasListItemAfter.d.ts +2 -0
  25. package/dist/packages/extension-list-keymap/src/listHelpers/hasListItemBefore.d.ts +2 -0
  26. package/dist/packages/extension-list-keymap/src/listHelpers/index.d.ts +10 -0
  27. package/dist/packages/extension-list-keymap/src/listHelpers/listItemHasSubList.d.ts +3 -0
  28. package/dist/packages/extension-list-keymap/src/listHelpers/nextListIsDeeper.d.ts +2 -0
  29. package/dist/packages/extension-list-keymap/src/listHelpers/nextListIsHigher.d.ts +2 -0
  30. package/package.json +46 -0
  31. package/src/index.ts +6 -0
  32. package/src/list-keymap.ts +94 -0
  33. package/src/listHelpers/findListItemPos.ts +31 -0
  34. package/src/listHelpers/getNextListDepth.ts +16 -0
  35. package/src/listHelpers/handleBackspace.ts +76 -0
  36. package/src/listHelpers/handleDelete.ts +38 -0
  37. package/src/listHelpers/hasListBefore.ts +15 -0
  38. package/src/listHelpers/hasListItemAfter.ts +17 -0
  39. package/src/listHelpers/hasListItemBefore.ts +17 -0
  40. package/src/listHelpers/index.ts +10 -0
  41. package/src/listHelpers/listItemHasSubList.ts +22 -0
  42. package/src/listHelpers/nextListIsDeeper.ts +19 -0
  43. package/src/listHelpers/nextListIsHigher.ts +19 -0
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@tiptap/extension-list-keymap",
3
+ "description": "list keymap extension for tiptap",
4
+ "version": "2.1.0-rc.13",
5
+ "homepage": "https://tiptap.dev",
6
+ "keywords": [
7
+ "tiptap",
8
+ "tiptap extension"
9
+ ],
10
+ "license": "MIT",
11
+ "funding": {
12
+ "type": "github",
13
+ "url": "https://github.com/sponsors/ueberdosis"
14
+ },
15
+ "type": "module",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./dist/packages/extension-list-keymap/src/index.d.ts",
19
+ "import": "./dist/index.js",
20
+ "require": "./dist/index.cjs"
21
+ }
22
+ },
23
+ "main": "dist/index.cjs",
24
+ "module": "dist/index.js",
25
+ "umd": "dist/index.umd.js",
26
+ "types": "dist/packages/extension-list-keymap/src/index.d.ts",
27
+ "files": [
28
+ "src",
29
+ "dist"
30
+ ],
31
+ "devDependencies": {
32
+ "@tiptap/core": "^2.1.0-rc.13"
33
+ },
34
+ "peerDependencies": {
35
+ "@tiptap/core": "^2.0.0"
36
+ },
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "https://github.com/ueberdosis/tiptap",
40
+ "directory": "packages/extension-list-keymap"
41
+ },
42
+ "scripts": {
43
+ "clean": "rm -rf dist",
44
+ "build": "npm run clean && rollup -c"
45
+ }
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ import { ListKeymap } from './list-keymap.js'
2
+
3
+ export * from './list-keymap.js'
4
+ export * as listHelpers from './listHelpers/index.js'
5
+
6
+ export default ListKeymap
@@ -0,0 +1,94 @@
1
+ import { Extension } from '@tiptap/core'
2
+
3
+ import { handleBackspace, handleDelete } from './listHelpers/index.js'
4
+
5
+ export type ListKeymapOptions = {
6
+ listTypes: Array<{
7
+ itemName: string,
8
+ wrapperNames: string[],
9
+ }>
10
+ }
11
+
12
+ export const ListKeymap = Extension.create<ListKeymapOptions>({
13
+ name: 'listKeymap',
14
+
15
+ addOptions() {
16
+ return {
17
+ listTypes: [
18
+ {
19
+ itemName: 'listItem',
20
+ wrapperNames: ['bulletList', 'orderedList'],
21
+ },
22
+ {
23
+ itemName: 'taskItem',
24
+ wrapperNames: ['taskList'],
25
+ },
26
+ ],
27
+ }
28
+ },
29
+
30
+ addKeyboardShortcuts() {
31
+ return {
32
+ Delete: ({ editor }) => {
33
+ let handled = false
34
+
35
+ this.options.listTypes.forEach(({ itemName }) => {
36
+ if (editor.state.schema.nodes[itemName] === undefined) {
37
+ return
38
+ }
39
+
40
+ if (handleDelete(editor, itemName)) {
41
+ handled = true
42
+ }
43
+ })
44
+
45
+ return handled
46
+ },
47
+ 'Mod-Delete': ({ editor }) => {
48
+ let handled = false
49
+
50
+ this.options.listTypes.forEach(({ itemName }) => {
51
+ if (editor.state.schema.nodes[itemName] === undefined) {
52
+ return
53
+ }
54
+
55
+ if (handleDelete(editor, itemName)) {
56
+ handled = true
57
+ }
58
+ })
59
+
60
+ return handled
61
+ },
62
+ Backspace: ({ editor }) => {
63
+ let handled = false
64
+
65
+ this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
66
+ if (editor.state.schema.nodes[itemName] === undefined) {
67
+ return
68
+ }
69
+
70
+ if (handleBackspace(editor, itemName, wrapperNames)) {
71
+ handled = true
72
+ }
73
+ })
74
+
75
+ return handled
76
+ },
77
+ 'Mod-Backspace': ({ editor }) => {
78
+ let handled = false
79
+
80
+ this.options.listTypes.forEach(({ itemName, wrapperNames }) => {
81
+ if (editor.state.schema.nodes[itemName] === undefined) {
82
+ return
83
+ }
84
+
85
+ if (handleBackspace(editor, itemName, wrapperNames)) {
86
+ handled = true
87
+ }
88
+ })
89
+
90
+ return handled
91
+ },
92
+ }
93
+ },
94
+ })
@@ -0,0 +1,31 @@
1
+ import { NodeType } from '@tiptap/pm/model'
2
+ import { EditorState } from '@tiptap/pm/state'
3
+
4
+ import { getNodeType } from '../../../core/src/helpers/getNodeType.js'
5
+
6
+ export const findListItemPos = (typeOrName: string | NodeType, state: EditorState) => {
7
+ const { $from } = state.selection
8
+ const nodeType = getNodeType(typeOrName, state.schema)
9
+
10
+ let currentNode = null
11
+ let currentDepth = $from.depth
12
+ let currentPos = $from.pos
13
+ let targetDepth: number | null = null
14
+
15
+ while (currentDepth > 0 && targetDepth === null) {
16
+ currentNode = $from.node(currentDepth)
17
+
18
+ if (currentNode.type === nodeType) {
19
+ targetDepth = currentDepth
20
+ } else {
21
+ currentDepth -= 1
22
+ currentPos -= 1
23
+ }
24
+ }
25
+
26
+ if (targetDepth === null) {
27
+ return null
28
+ }
29
+
30
+ return { $pos: state.doc.resolve(currentPos), depth: targetDepth }
31
+ }
@@ -0,0 +1,16 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ import { getNodeAtPosition } from '../../../core/src/helpers/getNodeAtPosition.js'
4
+ import { findListItemPos } from './findListItemPos.js'
5
+
6
+ export const getNextListDepth = (typeOrName: string, state: EditorState) => {
7
+ const listItemPos = findListItemPos(typeOrName, state)
8
+
9
+ if (!listItemPos) {
10
+ return false
11
+ }
12
+
13
+ const [, depth] = getNodeAtPosition(state, typeOrName, listItemPos.$pos.pos + 4)
14
+
15
+ return depth
16
+ }
@@ -0,0 +1,76 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+
3
+ import { Editor } from '../../../core/src/Editor.js'
4
+ import { isAtStartOfNode } from '../../../core/src/helpers/isAtStartOfNode.js'
5
+ import { isNodeActive } from '../../../core/src/helpers/isNodeActive.js'
6
+ import { findListItemPos } from './findListItemPos.js'
7
+ import { hasListBefore } from './hasListBefore.js'
8
+ import { hasListItemBefore } from './hasListItemBefore.js'
9
+ import { listItemHasSubList } from './listItemHasSubList.js'
10
+
11
+ export const handleBackspace = (editor: Editor, name: string, parentListTypes: string[]) => {
12
+ // this is required to still handle the undo handling
13
+ if (editor.commands.undoInputRule()) {
14
+ return true
15
+ }
16
+
17
+ // if the current item is NOT inside a list item &
18
+ // the previous item is a list (orderedList or bulletList)
19
+ // move the cursor into the list and delete the current item
20
+ if (!isNodeActive(editor.state, name) && hasListBefore(editor.state, name, parentListTypes)) {
21
+ const { $anchor } = editor.state.selection
22
+
23
+ const $listPos = editor.state.doc.resolve($anchor.before() - 1)
24
+
25
+ const listDescendants: Array<{ node: Node, pos: number }> = []
26
+
27
+ $listPos.node().descendants((node, pos) => {
28
+ if (node.type.name === name) {
29
+ listDescendants.push({ node, pos })
30
+ }
31
+ })
32
+
33
+ const lastItem = listDescendants.at(-1)
34
+
35
+ if (!lastItem) {
36
+ return false
37
+ }
38
+
39
+ const $lastItemPos = editor.state.doc.resolve($listPos.start() + lastItem.pos + 1)
40
+
41
+ return editor.chain().cut({ from: $anchor.start() - 1, to: $anchor.end() + 1 }, $lastItemPos.end()).joinForward().run()
42
+ }
43
+
44
+ // if the cursor is not inside the current node type
45
+ // do nothing and proceed
46
+ if (!isNodeActive(editor.state, name)) {
47
+ return false
48
+ }
49
+
50
+ // if the cursor is not at the start of a node
51
+ // do nothing and proceed
52
+ if (!isAtStartOfNode(editor.state)) {
53
+ return false
54
+ }
55
+
56
+ const listItemPos = findListItemPos(name, editor.state)
57
+
58
+ if (!listItemPos) {
59
+ return false
60
+ }
61
+
62
+ const $prev = editor.state.doc.resolve(listItemPos.$pos.pos - 2)
63
+ const prevNode = $prev.node(listItemPos.depth)
64
+
65
+ const previousListItemHasSubList = listItemHasSubList(name, editor.state, prevNode)
66
+
67
+ // if the previous item is a list item and doesn't have a sublist, join the list items
68
+ if (hasListItemBefore(name, editor.state) && !previousListItemHasSubList) {
69
+ return editor.commands.joinItemBackward()
70
+ }
71
+
72
+ // otherwise in the end, a backspace should
73
+ // always just lift the list item if
74
+ // joining / merging is not possible
75
+ return editor.chain().liftListItem(name).run()
76
+ }
@@ -0,0 +1,38 @@
1
+ import { Editor } from '../../../core/src/Editor.js'
2
+ import { isAtEndOfNode } from '../../../core/src/helpers/isAtEndOfNode.js'
3
+ import { isNodeActive } from '../../../core/src/helpers/isNodeActive.js'
4
+ import { nextListIsDeeper } from './nextListIsDeeper.js'
5
+ import { nextListIsHigher } from './nextListIsHigher.js'
6
+
7
+ export const handleDelete = (editor: Editor, name: string) => {
8
+ // if the cursor is not inside the current node type
9
+ // do nothing and proceed
10
+ if (!isNodeActive(editor.state, name)) {
11
+ return false
12
+ }
13
+
14
+ // if the cursor is not at the end of a node
15
+ // do nothing and proceed
16
+ if (!isAtEndOfNode(editor.state, name)) {
17
+ return false
18
+ }
19
+
20
+ // check if the next node is a list with a deeper depth
21
+ if (nextListIsDeeper(name, editor.state)) {
22
+ return editor
23
+ .chain()
24
+ .focus(editor.state.selection.from + 4)
25
+ .lift(name)
26
+ .joinBackward()
27
+ .run()
28
+ }
29
+
30
+ if (nextListIsHigher(name, editor.state)) {
31
+ return editor.chain()
32
+ .joinForward()
33
+ .joinBackward()
34
+ .run()
35
+ }
36
+
37
+ return editor.commands.joinItemForward()
38
+ }
@@ -0,0 +1,15 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ export const hasListBefore = (editorState: EditorState, name: string, parentListTypes: string[]) => {
4
+ const { $anchor } = editorState.selection
5
+
6
+ const previousNodePos = Math.max(0, $anchor.pos - 2)
7
+
8
+ const previousNode = editorState.doc.resolve(previousNodePos).node()
9
+
10
+ if (!previousNode || !parentListTypes.includes(previousNode.type.name)) {
11
+ return false
12
+ }
13
+
14
+ return true
15
+ }
@@ -0,0 +1,17 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ export const hasListItemAfter = (typeOrName: string, state: EditorState): boolean => {
4
+ const { $anchor } = state.selection
5
+
6
+ const $targetPos = state.doc.resolve($anchor.pos - $anchor.parentOffset - 2)
7
+
8
+ if ($targetPos.index() === $targetPos.parent.childCount - 1) {
9
+ return false
10
+ }
11
+
12
+ if ($targetPos.nodeAfter?.type.name !== typeOrName) {
13
+ return false
14
+ }
15
+
16
+ return true
17
+ }
@@ -0,0 +1,17 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ export const hasListItemBefore = (typeOrName: string, state: EditorState): boolean => {
4
+ const { $anchor } = state.selection
5
+
6
+ const $targetPos = state.doc.resolve($anchor.pos - 2)
7
+
8
+ if ($targetPos.index() === 0) {
9
+ return false
10
+ }
11
+
12
+ if ($targetPos.nodeBefore?.type.name !== typeOrName) {
13
+ return false
14
+ }
15
+
16
+ return true
17
+ }
@@ -0,0 +1,10 @@
1
+ export * from './findListItemPos.js'
2
+ export * from './getNextListDepth.js'
3
+ export * from './handleBackspace.js'
4
+ export * from './handleDelete.js'
5
+ export * from './hasListBefore.js'
6
+ export * from './hasListItemAfter.js'
7
+ export * from './hasListItemBefore.js'
8
+ export * from './listItemHasSubList.js'
9
+ export * from './nextListIsDeeper.js'
10
+ export * from './nextListIsHigher.js'
@@ -0,0 +1,22 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+ import { EditorState } from '@tiptap/pm/state'
3
+
4
+ import { getNodeType } from '../../../core/src/helpers/getNodeType.js'
5
+
6
+ export const listItemHasSubList = (typeOrName: string, state: EditorState, node?: Node) => {
7
+ if (!node) {
8
+ return false
9
+ }
10
+
11
+ const nodeType = getNodeType(typeOrName, state.schema)
12
+
13
+ let hasSubList = false
14
+
15
+ node.descendants(child => {
16
+ if (child.type === nodeType) {
17
+ hasSubList = true
18
+ }
19
+ })
20
+
21
+ return hasSubList
22
+ }
@@ -0,0 +1,19 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ import { findListItemPos } from './findListItemPos.js'
4
+ import { getNextListDepth } from './getNextListDepth.js'
5
+
6
+ export const nextListIsDeeper = (typeOrName: string, state: EditorState) => {
7
+ const listDepth = getNextListDepth(typeOrName, state)
8
+ const listItemPos = findListItemPos(typeOrName, state)
9
+
10
+ if (!listItemPos || !listDepth) {
11
+ return false
12
+ }
13
+
14
+ if (listDepth > listItemPos.depth) {
15
+ return true
16
+ }
17
+
18
+ return false
19
+ }
@@ -0,0 +1,19 @@
1
+ import { EditorState } from '@tiptap/pm/state'
2
+
3
+ import { findListItemPos } from './findListItemPos.js'
4
+ import { getNextListDepth } from './getNextListDepth.js'
5
+
6
+ export const nextListIsHigher = (typeOrName: string, state: EditorState) => {
7
+ const listDepth = getNextListDepth(typeOrName, state)
8
+ const listItemPos = findListItemPos(typeOrName, state)
9
+
10
+ if (!listItemPos || !listDepth) {
11
+ return false
12
+ }
13
+
14
+ if (listDepth < listItemPos.depth) {
15
+ return true
16
+ }
17
+
18
+ return false
19
+ }