@prosekit/core 0.8.3 → 0.8.5

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 (170) hide show
  1. package/dist/editor-KZlceNQ1.d.ts +722 -0
  2. package/dist/editor-KZlceNQ1.d.ts.map +1 -0
  3. package/dist/{editor-DlGlYOp-.js → editor-TvRTsFdO.js} +102 -196
  4. package/dist/editor-TvRTsFdO.js.map +1 -0
  5. package/dist/prosekit-core-test.d.ts +20 -19
  6. package/dist/prosekit-core-test.d.ts.map +1 -0
  7. package/dist/prosekit-core-test.js +5 -8
  8. package/dist/prosekit-core-test.js.map +1 -0
  9. package/dist/prosekit-core.d.ts +797 -792
  10. package/dist/prosekit-core.d.ts.map +1 -0
  11. package/dist/prosekit-core.js +42 -79
  12. package/dist/prosekit-core.js.map +1 -0
  13. package/package.json +14 -12
  14. package/src/commands/add-mark.ts +53 -0
  15. package/src/commands/expand-mark.ts +96 -0
  16. package/src/commands/insert-default-block.spec.ts +102 -0
  17. package/src/commands/insert-default-block.ts +49 -0
  18. package/src/commands/insert-node.ts +71 -0
  19. package/src/commands/insert-text.ts +24 -0
  20. package/src/commands/remove-mark.ts +54 -0
  21. package/src/commands/remove-node.ts +43 -0
  22. package/src/commands/select-all.ts +16 -0
  23. package/src/commands/set-block-type.ts +64 -0
  24. package/src/commands/set-node-attrs.ts +68 -0
  25. package/src/commands/toggle-mark.ts +65 -0
  26. package/src/commands/toggle-node.ts +47 -0
  27. package/src/commands/toggle-wrap.spec.ts +35 -0
  28. package/src/commands/toggle-wrap.ts +42 -0
  29. package/src/commands/unset-block-type.spec.ts +49 -0
  30. package/src/commands/unset-block-type.ts +84 -0
  31. package/src/commands/unset-mark.spec.ts +35 -0
  32. package/src/commands/unset-mark.ts +38 -0
  33. package/src/commands/wrap.ts +50 -0
  34. package/src/editor/action.spec.ts +143 -0
  35. package/src/editor/action.ts +248 -0
  36. package/src/editor/editor.spec.ts +186 -0
  37. package/src/editor/editor.ts +563 -0
  38. package/src/editor/union.spec.ts +108 -0
  39. package/src/editor/union.ts +47 -0
  40. package/src/editor/with-priority.ts +25 -0
  41. package/src/error.ts +28 -0
  42. package/src/extensions/clipboard-serializer.ts +107 -0
  43. package/src/extensions/command.ts +121 -0
  44. package/src/extensions/default-state.spec.ts +60 -0
  45. package/src/extensions/default-state.ts +76 -0
  46. package/src/extensions/doc.ts +31 -0
  47. package/src/extensions/events/doc-change.ts +34 -0
  48. package/src/extensions/events/dom-event.spec.ts +70 -0
  49. package/src/extensions/events/dom-event.ts +117 -0
  50. package/src/extensions/events/editor-event.ts +293 -0
  51. package/src/extensions/events/focus.spec.ts +50 -0
  52. package/src/extensions/events/focus.ts +28 -0
  53. package/src/extensions/events/plugin-view.ts +132 -0
  54. package/src/extensions/history.ts +81 -0
  55. package/src/extensions/keymap-base.ts +60 -0
  56. package/src/extensions/keymap.spec.ts +125 -0
  57. package/src/extensions/keymap.ts +96 -0
  58. package/src/extensions/mark-spec.spec.ts +177 -0
  59. package/src/extensions/mark-spec.ts +181 -0
  60. package/src/extensions/mark-view-effect.ts +85 -0
  61. package/src/extensions/mark-view.ts +43 -0
  62. package/src/extensions/node-spec.spec.ts +224 -0
  63. package/src/extensions/node-spec.ts +199 -0
  64. package/src/extensions/node-view-effect.ts +85 -0
  65. package/src/extensions/node-view.ts +43 -0
  66. package/src/extensions/paragraph.ts +61 -0
  67. package/src/extensions/plugin.spec.ts +153 -0
  68. package/src/extensions/plugin.ts +81 -0
  69. package/src/extensions/text.ts +34 -0
  70. package/src/facets/base-extension.ts +54 -0
  71. package/src/facets/command.ts +21 -0
  72. package/src/facets/facet-extension.spec.ts +173 -0
  73. package/src/facets/facet-extension.ts +53 -0
  74. package/src/facets/facet-node.spec.ts +265 -0
  75. package/src/facets/facet-node.ts +185 -0
  76. package/src/facets/facet-types.ts +9 -0
  77. package/src/facets/facet.spec.ts +76 -0
  78. package/src/facets/facet.ts +84 -0
  79. package/src/facets/root.ts +44 -0
  80. package/src/facets/schema-spec.ts +30 -0
  81. package/src/facets/schema.ts +26 -0
  82. package/src/facets/state.spec.ts +53 -0
  83. package/src/facets/state.ts +85 -0
  84. package/src/facets/union-extension.ts +41 -0
  85. package/src/index.ts +302 -0
  86. package/src/test/index.ts +4 -0
  87. package/src/test/test-builder.ts +68 -0
  88. package/src/test/test-editor.spec.ts +104 -0
  89. package/src/test/test-editor.ts +113 -0
  90. package/src/testing/index.ts +283 -0
  91. package/src/testing/keyboard.ts +5 -0
  92. package/src/types/any-function.ts +4 -0
  93. package/src/types/assert-type-equal.ts +8 -0
  94. package/src/types/attrs.ts +32 -0
  95. package/src/types/base-node-view-options.ts +33 -0
  96. package/src/types/dom-node.ts +1 -0
  97. package/src/types/extension-command.ts +52 -0
  98. package/src/types/extension-mark.ts +15 -0
  99. package/src/types/extension-node.ts +15 -0
  100. package/src/types/extension.spec.ts +56 -0
  101. package/src/types/extension.ts +168 -0
  102. package/src/types/model.ts +54 -0
  103. package/src/types/object-entries.ts +13 -0
  104. package/src/types/pick-string-literal.spec.ts +10 -0
  105. package/src/types/pick-string-literal.ts +6 -0
  106. package/src/types/pick-sub-type.spec.ts +20 -0
  107. package/src/types/pick-sub-type.ts +6 -0
  108. package/src/types/priority.ts +12 -0
  109. package/src/types/setter.ts +4 -0
  110. package/src/types/simplify-deeper.spec.ts +40 -0
  111. package/src/types/simplify-deeper.ts +6 -0
  112. package/src/types/simplify-union.spec.ts +21 -0
  113. package/src/types/simplify-union.ts +11 -0
  114. package/src/utils/array-grouping.spec.ts +29 -0
  115. package/src/utils/array-grouping.ts +25 -0
  116. package/src/utils/array.ts +21 -0
  117. package/src/utils/assert.ts +13 -0
  118. package/src/utils/attrs-match.ts +20 -0
  119. package/src/utils/can-use-regex-lookbehind.ts +12 -0
  120. package/src/utils/clsx.spec.ts +14 -0
  121. package/src/utils/clsx.ts +14 -0
  122. package/src/utils/collect-children.ts +21 -0
  123. package/src/utils/collect-nodes.ts +37 -0
  124. package/src/utils/combine-event-handlers.spec.ts +27 -0
  125. package/src/utils/combine-event-handlers.ts +27 -0
  126. package/src/utils/contains-inline-node.ts +17 -0
  127. package/src/utils/deep-equals.spec.ts +26 -0
  128. package/src/utils/deep-equals.ts +29 -0
  129. package/src/utils/default-block-at.ts +15 -0
  130. package/src/utils/editor-content.spec.ts +47 -0
  131. package/src/utils/editor-content.ts +77 -0
  132. package/src/utils/env.ts +6 -0
  133. package/src/utils/find-parent-node-of-type.ts +29 -0
  134. package/src/utils/find-parent-node.spec.ts +68 -0
  135. package/src/utils/find-parent-node.ts +55 -0
  136. package/src/utils/get-custom-selection.ts +19 -0
  137. package/src/utils/get-dom-api.ts +56 -0
  138. package/src/utils/get-id.spec.ts +14 -0
  139. package/src/utils/get-id.ts +13 -0
  140. package/src/utils/get-mark-type.ts +20 -0
  141. package/src/utils/get-node-type.ts +20 -0
  142. package/src/utils/get-node-types.ts +19 -0
  143. package/src/utils/includes-mark.ts +18 -0
  144. package/src/utils/is-at-block-start.ts +26 -0
  145. package/src/utils/is-in-code-block.ts +18 -0
  146. package/src/utils/is-mark-absent.spec.ts +53 -0
  147. package/src/utils/is-mark-absent.ts +42 -0
  148. package/src/utils/is-mark-active.ts +27 -0
  149. package/src/utils/is-node-active.ts +25 -0
  150. package/src/utils/is-subset.spec.ts +12 -0
  151. package/src/utils/is-subset.ts +11 -0
  152. package/src/utils/maybe-run.spec.ts +39 -0
  153. package/src/utils/maybe-run.ts +11 -0
  154. package/src/utils/merge-objects.spec.ts +30 -0
  155. package/src/utils/merge-objects.ts +11 -0
  156. package/src/utils/merge-specs.ts +35 -0
  157. package/src/utils/object-equal.spec.ts +26 -0
  158. package/src/utils/object-equal.ts +28 -0
  159. package/src/utils/output-spec.test.ts +95 -0
  160. package/src/utils/output-spec.ts +130 -0
  161. package/src/utils/parse.spec.ts +46 -0
  162. package/src/utils/parse.ts +321 -0
  163. package/src/utils/remove-undefined-values.spec.ts +15 -0
  164. package/src/utils/remove-undefined-values.ts +9 -0
  165. package/src/utils/set-selection-around.ts +11 -0
  166. package/src/utils/type-assertion.ts +91 -0
  167. package/src/utils/unicode.spec.ts +10 -0
  168. package/src/utils/unicode.ts +4 -0
  169. package/src/utils/with-skip-code-block.ts +15 -0
  170. package/dist/editor-OUH5V8BA.d.ts +0 -754
@@ -0,0 +1,49 @@
1
+ import {
2
+ TextSelection,
3
+ type Command,
4
+ } from '@prosekit/pm/state'
5
+
6
+ import { defaultBlockAt } from '../utils/default-block-at'
7
+
8
+ /**
9
+ * @public
10
+ */
11
+ export interface InsertDefaultBlockOptions {
12
+ /**
13
+ * The position to insert the node at. By default it will insert after the
14
+ * current selection.
15
+ */
16
+ pos?: number
17
+ }
18
+
19
+ /**
20
+ * Returns a command that inserts a default block after current selection or at
21
+ * the given position.
22
+ *
23
+ * @public
24
+ */
25
+ export function insertDefaultBlock(
26
+ options?: InsertDefaultBlockOptions,
27
+ ): Command {
28
+ return (state, dispatch) => {
29
+ const $pos = options?.pos == null
30
+ ? state.selection.$to
31
+ : state.doc.resolve(options.pos)
32
+ const depth = $pos.parent.isTextblock ? $pos.depth - 1 : $pos.depth
33
+ const parent = $pos.node(depth)
34
+ const index = $pos.indexAfter(depth)
35
+ const type = defaultBlockAt(parent.contentMatchAt(index))
36
+ if (!type) return false
37
+ if (dispatch) {
38
+ const pos = $pos.posAtIndex(index, depth)
39
+ const node = type.createAndFill()
40
+ if (!node) return false
41
+ const tr = state.tr.insert(pos, node)
42
+ const selection = TextSelection.findFrom(tr.doc.resolve(pos), 1)
43
+ if (!selection) return false
44
+ tr.setSelection(selection)
45
+ dispatch(tr.scrollIntoView())
46
+ }
47
+ return true
48
+ }
49
+ }
@@ -0,0 +1,71 @@
1
+ import type {
2
+ Attrs,
3
+ NodeType,
4
+ ProseMirrorNode,
5
+ } from '@prosekit/pm/model'
6
+ import type { Command } from '@prosekit/pm/state'
7
+ import { insertPoint } from '@prosekit/pm/transform'
8
+
9
+ import { assert } from '../utils/assert'
10
+ import { getNodeType } from '../utils/get-node-type'
11
+ import { setSelectionAround } from '../utils/set-selection-around'
12
+
13
+ /**
14
+ * @public
15
+ */
16
+ export interface InsertNodeOptions {
17
+ /**
18
+ * The node to insert. Either this or `type` must be provided.
19
+ */
20
+ node?: ProseMirrorNode
21
+
22
+ /**
23
+ * The type of the node to insert. Either this or `node` must be provided.
24
+ */
25
+ type?: string | NodeType
26
+
27
+ /**
28
+ * When `type` is provided, the attributes of the node to insert.
29
+ */
30
+ attrs?: Attrs
31
+
32
+ /**
33
+ * The position to insert the node at. By default it will be the anchor
34
+ * position of current selection.
35
+ */
36
+ pos?: number
37
+ }
38
+
39
+ /**
40
+ * Returns a command that inserts the given node at the current selection or at
41
+ * the given position.
42
+ *
43
+ * @public
44
+ */
45
+ function insertNode(options: InsertNodeOptions): Command {
46
+ return (state, dispatch) => {
47
+ const node = options.node
48
+ ? options.node
49
+ : options.type
50
+ ? getNodeType(state.schema, options.type).createAndFill(options.attrs)
51
+ : null
52
+
53
+ assert(node, 'You must provide either a node or a type')
54
+
55
+ const insertPos = insertPoint(
56
+ state.doc,
57
+ options.pos ?? state.selection.anchor,
58
+ node.type,
59
+ )
60
+ if (insertPos == null) return false
61
+
62
+ if (dispatch) {
63
+ const tr = state.tr.insert(insertPos, node)
64
+ setSelectionAround(tr, insertPos + node.nodeSize)
65
+ dispatch(tr)
66
+ }
67
+ return true
68
+ }
69
+ }
70
+
71
+ export { insertNode }
@@ -0,0 +1,24 @@
1
+ import type { Command } from '@prosekit/pm/state'
2
+
3
+ /**
4
+ * @public
5
+ */
6
+ export type InsertTextOptions = {
7
+ text: string
8
+ from?: number
9
+ to?: number
10
+ }
11
+
12
+ /**
13
+ * Returns a command that inserts the given text.
14
+ *
15
+ * @public
16
+ */
17
+ export function insertText({ text, from, to }: InsertTextOptions): Command {
18
+ return (state, dispatch) => {
19
+ if (text) {
20
+ dispatch?.(state.tr.insertText(text, from, to))
21
+ }
22
+ return true
23
+ }
24
+ }
@@ -0,0 +1,54 @@
1
+ import type {
2
+ Attrs,
3
+ MarkType,
4
+ } from '@prosekit/pm/model'
5
+ import type { Command } from '@prosekit/pm/state'
6
+
7
+ import type { CommandCreator } from '../types/extension-command'
8
+ import { getMarkType } from '../utils/get-mark-type'
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ export interface RemoveMarkOptions {
14
+ /**
15
+ * The type of the mark to remove.
16
+ */
17
+ type: string | MarkType
18
+
19
+ /**
20
+ * If attrs is given, remove precisely the mark with the given attrs. Otherwise, remove all marks of the given type.
21
+ */
22
+ attrs?: Attrs | null
23
+
24
+ /**
25
+ * The start position of the document. By default it will be the start position of current selection.
26
+ */
27
+ from?: number
28
+
29
+ /**
30
+ * The end position of the document. By default it will be the end position of current selection.
31
+ */
32
+ to?: number
33
+ }
34
+
35
+ /**
36
+ * Returns a command that removes the given mark.
37
+ *
38
+ * @public
39
+ */
40
+ export function removeMark(options: RemoveMarkOptions): Command {
41
+ return (state, dispatch) => {
42
+ const markType = getMarkType(state.schema, options.type)
43
+ const mark = options.attrs ? markType.create(options.attrs) : markType
44
+ const from = options.from ?? state.selection.from
45
+ const to = options.to ?? state.selection.to
46
+ if (from > to) {
47
+ return false
48
+ }
49
+ dispatch?.(state.tr.removeMark(from, to, mark))
50
+ return true
51
+ }
52
+ }
53
+
54
+ removeMark satisfies CommandCreator
@@ -0,0 +1,43 @@
1
+ import type { NodeType } from '@prosekit/pm/model'
2
+ import type { Command } from '@prosekit/pm/state'
3
+
4
+ import type { CommandCreator } from '../types/extension-command'
5
+ import { findParentNodeOfType } from '../utils/find-parent-node-of-type'
6
+
7
+ /**
8
+ * @public
9
+ */
10
+ export interface RemoveNodeOptions {
11
+ /**
12
+ * The type of the node to remove.
13
+ */
14
+ type: string | NodeType
15
+
16
+ /**
17
+ * The document position to start searching node. By default it will be the
18
+ * anchor position of current selection.
19
+ */
20
+ pos?: number
21
+ }
22
+
23
+ /**
24
+ * Returns a command to remove the nearest ancestor node of a specific type from the current position.
25
+ *
26
+ * @public
27
+ */
28
+ export function removeNode(options: RemoveNodeOptions): Command {
29
+ return (state, dispatch) => {
30
+ const $pos = typeof options.pos === 'number'
31
+ ? state.doc.resolve(options.pos)
32
+ : state.selection.$anchor
33
+
34
+ const found = findParentNodeOfType(options.type, $pos)
35
+ if (!found) return false
36
+
37
+ const { pos, node } = found
38
+ dispatch?.(state.tr.delete(pos, pos + node.nodeSize))
39
+ return true
40
+ }
41
+ }
42
+
43
+ removeNode satisfies CommandCreator
@@ -0,0 +1,16 @@
1
+ import {
2
+ AllSelection,
3
+ type Command,
4
+ } from '@prosekit/pm/state'
5
+
6
+ /**
7
+ * Returns a command that selects the whole document.
8
+ *
9
+ * @public
10
+ */
11
+ export function selectAll(): Command {
12
+ return (state, dispatch) => {
13
+ dispatch?.(state.tr.setSelection(new AllSelection(state.doc)))
14
+ return true
15
+ }
16
+ }
@@ -0,0 +1,64 @@
1
+ import type {
2
+ Attrs,
3
+ NodeType,
4
+ } from '@prosekit/pm/model'
5
+ import type { Command } from '@prosekit/pm/state'
6
+
7
+ import { getCustomSelection } from '../utils/get-custom-selection'
8
+ import { getNodeType } from '../utils/get-node-type'
9
+
10
+ /**
11
+ * @public
12
+ */
13
+ export interface SetBlockTypeOptions {
14
+ type: NodeType | string
15
+ attrs?: Attrs | null
16
+ from?: number
17
+ to?: number
18
+ }
19
+
20
+ /**
21
+ * Returns a command that tries to set the selected textblocks to the given node
22
+ * type with the given attributes.
23
+ *
24
+ * @public
25
+ */
26
+ export function setBlockType(options: SetBlockTypeOptions): Command {
27
+ return (state, dispatch) => {
28
+ const nodeType = getNodeType(state.schema, options.type)
29
+ const selection = getCustomSelection(state, options.from, options.to)
30
+ const attrs = options.attrs
31
+
32
+ let applicable = false
33
+ for (let i = 0; i < selection.ranges.length && !applicable; i++) {
34
+ const {
35
+ $from: { pos: from },
36
+ $to: { pos: to },
37
+ } = selection.ranges[i]
38
+ state.doc.nodesBetween(from, to, (node, pos) => {
39
+ if (applicable) return false
40
+ if (!node.isTextblock || node.hasMarkup(nodeType, attrs)) return
41
+ if (node.type == nodeType) {
42
+ applicable = true
43
+ } else {
44
+ const $pos = state.doc.resolve(pos),
45
+ index = $pos.index()
46
+ applicable = $pos.parent.canReplaceWith(index, index + 1, nodeType)
47
+ }
48
+ })
49
+ }
50
+ if (!applicable) return false
51
+ if (dispatch) {
52
+ const tr = state.tr
53
+ for (const range of selection.ranges) {
54
+ const {
55
+ $from: { pos: from },
56
+ $to: { pos: to },
57
+ } = range
58
+ tr.setBlockType(from, to, nodeType, attrs)
59
+ }
60
+ dispatch(tr.scrollIntoView())
61
+ }
62
+ return true
63
+ }
64
+ }
@@ -0,0 +1,68 @@
1
+ import type {
2
+ Attrs,
3
+ NodeType,
4
+ } from '@prosekit/pm/model'
5
+ import type { Command } from '@prosekit/pm/state'
6
+
7
+ import { getNodeTypes } from '../utils/get-node-types'
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export interface SetNodeAttrsOptions {
13
+ /**
14
+ * The type of node to set the attributes of.
15
+ *
16
+ * If current node is not of this type, the command will do nothing.
17
+ */
18
+ type: string | NodeType | string[] | NodeType[]
19
+
20
+ /**
21
+ * The attributes to set.
22
+ */
23
+ attrs: Attrs
24
+
25
+ /**
26
+ * The position of the node. Defaults to the position of the wrapping node
27
+ * containing the current selection.
28
+ */
29
+ pos?: number
30
+ }
31
+
32
+ /**
33
+ * Returns a command that set the attributes of the current node.
34
+ *
35
+ * @public
36
+ */
37
+ export function setNodeAttrs(options: SetNodeAttrsOptions): Command {
38
+ return (state, dispatch) => {
39
+ const nodeTypes = getNodeTypes(state.schema, options.type)
40
+ const from = options.pos ?? state.selection.from
41
+ const to = options.pos ?? state.selection.to
42
+ const positions: number[] = []
43
+
44
+ state.doc.nodesBetween(from, to, (node, pos) => {
45
+ if (nodeTypes.includes(node.type)) {
46
+ positions.push(pos)
47
+ }
48
+ if (!dispatch && positions.length > 0) {
49
+ return false
50
+ }
51
+ })
52
+
53
+ if (positions.length === 0) {
54
+ return false
55
+ }
56
+
57
+ if (dispatch) {
58
+ const { tr } = state
59
+ for (const pos of positions) {
60
+ for (const [key, value] of Object.entries(options.attrs)) {
61
+ tr.setNodeAttribute(pos, key, value)
62
+ }
63
+ }
64
+ dispatch(tr)
65
+ }
66
+ return true
67
+ }
68
+ }
@@ -0,0 +1,65 @@
1
+ import { toggleMark as baseToggleMark } from '@prosekit/pm/commands'
2
+ import type {
3
+ Attrs,
4
+ MarkType,
5
+ } from '@prosekit/pm/model'
6
+ import type { Command } from '@prosekit/pm/state'
7
+
8
+ import type { CommandCreator } from '../types/extension-command'
9
+ import { getMarkType } from '../utils/get-mark-type'
10
+
11
+ /**
12
+ * @public
13
+ */
14
+ export interface ToggleMarkOptions {
15
+ /**
16
+ * The mark type to toggle.
17
+ */
18
+ type: string | MarkType
19
+
20
+ /**
21
+ * The optional attributes to set on the mark.
22
+ */
23
+ attrs?: Attrs | null
24
+
25
+ /**
26
+ * Controls whether, when part of the selected range has the mark
27
+ * already and part doesn't, the mark is removed (`true`) or added
28
+ * (`false`).
29
+ *
30
+ * @default false
31
+ */
32
+ removeWhenPresent?: boolean
33
+
34
+ /**
35
+ * Whether the command should act on the content of inline nodes marked as
36
+ * [atoms](https://prosemirror.net/docs/ref/#model.NodeSpec.atom) that are
37
+ * completely covered by a selection range.
38
+ *
39
+ * @default true
40
+ */
41
+ enterInlineAtoms?: boolean
42
+ }
43
+
44
+ /**
45
+ * Returns a command that toggles the given mark with the given attributes.
46
+ *
47
+ * @param options
48
+ *
49
+ * @public
50
+ */
51
+ export function toggleMark({
52
+ type,
53
+ attrs,
54
+ removeWhenPresent = false,
55
+ enterInlineAtoms = true,
56
+ }: ToggleMarkOptions): Command {
57
+ return (state, dispatch, view) => {
58
+ return baseToggleMark(getMarkType(state.schema, type), attrs, {
59
+ removeWhenPresent,
60
+ enterInlineAtoms,
61
+ })(state, dispatch, view)
62
+ }
63
+ }
64
+
65
+ toggleMark satisfies CommandCreator
@@ -0,0 +1,47 @@
1
+ import { setBlockType } from '@prosekit/pm/commands'
2
+ import type {
3
+ Attrs,
4
+ NodeType,
5
+ } from '@prosekit/pm/model'
6
+ import type { Command } from '@prosekit/pm/state'
7
+
8
+ import { getNodeType } from '../utils/get-node-type'
9
+ import { isNodeActive } from '../utils/is-node-active'
10
+
11
+ /**
12
+ * @public
13
+ */
14
+ export interface ToggleNodeOptions {
15
+ /**
16
+ * The type of the node to toggle.
17
+ */
18
+ type: string | NodeType
19
+
20
+ /**
21
+ * The attributes of the node to toggle.
22
+ */
23
+ attrs?: Attrs | null
24
+ }
25
+
26
+ /**
27
+ * Returns a command that set the selected textblocks to the given node type
28
+ * with the given attributes.
29
+ *
30
+ * @param options
31
+ *
32
+ * @public
33
+ */
34
+ export function toggleNode({ type, attrs }: ToggleNodeOptions): Command {
35
+ return (state, dispatch, view) => {
36
+ if (isNodeActive(state, type, attrs)) {
37
+ const defaultType = state.schema.topNodeType.contentMatch.defaultType
38
+ if (!defaultType) {
39
+ return false
40
+ }
41
+ return setBlockType(defaultType)(state, dispatch, view)
42
+ } else {
43
+ const nodeType = getNodeType(state.schema, type)
44
+ return setBlockType(nodeType, attrs)(state, dispatch, view)
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,35 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+
7
+ import { setupTest } from '../testing'
8
+
9
+ describe('toggle-wrap', () => {
10
+ it('adds the node wrapping the selection', () => {
11
+ const { editor, n } = setupTest()
12
+
13
+ const doc1 = n.doc(n.paragraph('<a>hello'))
14
+ const doc2 = n.doc(n.blockquote(n.paragraph('<a>hello')))
15
+
16
+ editor.set(doc1)
17
+ expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
18
+
19
+ editor.commands.toggleWrap({ type: 'blockquote' })
20
+ expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
21
+ })
22
+
23
+ it('lift the wrapped node', () => {
24
+ const { editor, n } = setupTest()
25
+
26
+ const doc1 = n.doc(n.blockquote(n.paragraph('<a>hello')))
27
+ const doc2 = n.doc(n.paragraph('<a>hello'))
28
+
29
+ editor.set(doc1)
30
+ expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
31
+
32
+ editor.commands.toggleWrap({ type: 'paragraph' })
33
+ expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
34
+ })
35
+ })
@@ -0,0 +1,42 @@
1
+ import { lift } from '@prosekit/pm/commands'
2
+ import type {
3
+ Attrs,
4
+ NodeType,
5
+ } from '@prosekit/pm/model'
6
+ import type { Command } from '@prosekit/pm/state'
7
+
8
+ import { isNodeActive } from '../utils/is-node-active'
9
+
10
+ import { wrap } from './wrap'
11
+
12
+ export interface ToggleWrapOptions {
13
+ /**
14
+ * The type of the node to toggle.
15
+ */
16
+ type: string | NodeType
17
+
18
+ /**
19
+ * The attributes of the node to toggle.
20
+ */
21
+ attrs?: Attrs | null
22
+ }
23
+
24
+ /**
25
+ * Toggle between wrapping an inactive node with the provided node type, and
26
+ * lifting it up into its parent.
27
+ *
28
+ * @param options
29
+ *
30
+ * @public
31
+ */
32
+ export function toggleWrap(options: ToggleWrapOptions): Command {
33
+ const { type, attrs } = options
34
+
35
+ return (state, dispatch) => {
36
+ if (isNodeActive(state, type, attrs)) {
37
+ return lift(state, dispatch)
38
+ }
39
+
40
+ return wrap({ type, attrs })(state, dispatch)
41
+ }
42
+ }
@@ -0,0 +1,49 @@
1
+ import {
2
+ describe,
3
+ expect,
4
+ it,
5
+ } from 'vitest'
6
+
7
+ import { setupTest } from '../testing'
8
+
9
+ describe('unsetBlockType', () => {
10
+ it('can unset a single block', () => {
11
+ const { editor, n } = setupTest()
12
+
13
+ const doc1 = n.doc(n.heading('<a>1'), n.heading('2'))
14
+ const doc2 = n.doc(n.paragraph('<a>1'), n.heading('2'))
15
+
16
+ editor.set(doc1)
17
+ expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
18
+
19
+ editor.commands.unsetBlockType()
20
+ expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
21
+ })
22
+
23
+ it('can unset multiple blocks', () => {
24
+ const { editor, n } = setupTest()
25
+
26
+ const doc1 = n.doc(
27
+ n.heading('0'),
28
+ n.heading('<a>1'),
29
+ n.heading('2'),
30
+ n.heading('<b>3'),
31
+ n.heading('4'),
32
+ n.heading('5'),
33
+ )
34
+ const doc2 = n.doc(
35
+ n.heading('0'),
36
+ n.paragraph('<a>1'),
37
+ n.paragraph('2'),
38
+ n.paragraph('<b>3'),
39
+ n.heading('4'),
40
+ n.heading('5'),
41
+ )
42
+
43
+ editor.set(doc1)
44
+ expect(editor.state.doc.toJSON()).toEqual(doc1.toJSON())
45
+
46
+ editor.commands.unsetBlockType()
47
+ expect(editor.state.doc.toJSON()).toEqual(doc2.toJSON())
48
+ })
49
+ })