@prosekit/core 0.8.7 → 0.10.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/{editor-M9OimMiI.d.ts → editor-BULC1zqX.d.ts} +31 -71
- package/dist/editor-BULC1zqX.d.ts.map +1 -0
- package/dist/{editor-B0L9BgMi.js → editor-g-Rqn-ZE.js} +119 -136
- package/dist/editor-g-Rqn-ZE.js.map +1 -0
- package/dist/prosekit-core-test.d.ts +1 -2
- package/dist/prosekit-core-test.d.ts.map +1 -1
- package/dist/prosekit-core-test.js +1 -1
- package/dist/prosekit-core-test.js.map +1 -1
- package/dist/prosekit-core.d.ts +182 -202
- package/dist/prosekit-core.d.ts.map +1 -1
- package/dist/prosekit-core.js +543 -549
- package/dist/prosekit-core.js.map +1 -1
- package/package.json +9 -12
- package/src/commands/add-mark.ts +1 -4
- package/src/commands/expand-mark.ts +2 -9
- package/src/commands/insert-default-block.spec.ts +1 -5
- package/src/commands/insert-default-block.ts +1 -4
- package/src/commands/insert-node.ts +4 -8
- package/src/commands/remove-mark.ts +1 -4
- package/src/commands/remove-node.ts +2 -2
- package/src/commands/select-all.ts +3 -8
- package/src/commands/select-block.spec.ts +81 -0
- package/src/commands/select-block.ts +56 -0
- package/src/commands/set-block-type.ts +1 -4
- package/src/commands/set-node-attrs-between.spec.ts +221 -0
- package/src/commands/set-node-attrs-between.ts +77 -0
- package/src/commands/set-node-attrs.spec.ts +129 -0
- package/src/commands/set-node-attrs.ts +25 -26
- package/src/commands/toggle-mark.ts +1 -4
- package/src/commands/toggle-node.ts +2 -5
- package/src/commands/toggle-wrap.spec.ts +1 -5
- package/src/commands/toggle-wrap.ts +1 -4
- package/src/commands/unset-block-type.spec.ts +1 -5
- package/src/commands/unset-block-type.ts +2 -8
- package/src/commands/unset-mark.spec.ts +1 -5
- package/src/commands/wrap.ts +2 -10
- package/src/editor/action.spec.ts +2 -6
- package/src/editor/action.ts +2 -19
- package/src/editor/editor.spec.ts +2 -9
- package/src/editor/editor.ts +31 -77
- package/src/editor/union.spec.ts +1 -5
- package/src/editor/union.ts +1 -4
- package/src/extensions/clipboard-serializer.ts +4 -16
- package/src/extensions/command.ts +20 -48
- package/src/extensions/default-state.spec.ts +1 -9
- package/src/extensions/default-state.ts +6 -32
- package/src/extensions/events/dom-event.spec.ts +1 -6
- package/src/extensions/events/dom-event.ts +5 -20
- package/src/extensions/events/editor-event.ts +5 -17
- package/src/extensions/events/focus.spec.ts +1 -6
- package/src/extensions/events/plugin-view.ts +2 -9
- package/src/extensions/history.ts +3 -10
- package/src/extensions/keymap-base.spec.ts +89 -0
- package/src/extensions/keymap-base.ts +34 -13
- package/src/extensions/keymap.spec.ts +12 -22
- package/src/extensions/keymap.ts +16 -69
- package/src/extensions/mark-spec.spec.ts +5 -20
- package/src/extensions/mark-spec.ts +33 -41
- package/src/extensions/mark-view-effect.ts +3 -9
- package/src/extensions/mark-view.ts +2 -8
- package/src/extensions/node-spec.spec.ts +5 -21
- package/src/extensions/node-spec.ts +31 -33
- package/src/extensions/node-view-effect.ts +3 -9
- package/src/extensions/node-view.ts +2 -8
- package/src/extensions/plugin.spec.ts +3 -16
- package/src/extensions/plugin.ts +4 -14
- package/src/facets/base-extension.ts +1 -4
- package/src/facets/command.ts +10 -10
- package/src/facets/facet-extension.spec.ts +4 -15
- package/src/facets/facet-node.spec.ts +2 -9
- package/src/facets/facet-node.ts +4 -9
- package/src/facets/facet.spec.ts +1 -4
- package/src/facets/schema-spec.ts +2 -9
- package/src/facets/schema.ts +3 -12
- package/src/facets/state.spec.ts +8 -15
- package/src/facets/state.ts +5 -20
- package/src/facets/union-extension.ts +2 -8
- package/src/index.ts +42 -188
- package/src/test/index.ts +1 -4
- package/src/test/test-builder.ts +1 -4
- package/src/test/test-editor.spec.ts +1 -5
- package/src/test/test-editor.ts +5 -24
- package/src/testing/index.ts +18 -14
- package/src/types/extension-command.ts +0 -7
- package/src/types/extension.spec.ts +1 -4
- package/src/types/extension.ts +3 -29
- package/src/types/simplify-union.ts +1 -4
- package/src/utils/array-grouping.spec.ts +2 -15
- package/src/utils/array-grouping.ts +1 -14
- package/src/utils/array.ts +0 -4
- package/src/utils/attrs-match.ts +1 -5
- package/src/utils/clsx.spec.ts +1 -4
- package/src/utils/combine-event-handlers.spec.ts +1 -5
- package/src/utils/combine-event-handlers.ts +4 -6
- package/src/utils/default-block-at.ts +1 -4
- package/src/utils/editor-content.spec.ts +1 -4
- package/src/utils/editor-content.ts +7 -19
- package/src/utils/find-node.ts +65 -0
- package/src/utils/find-parent-node-of-type.ts +6 -12
- package/src/utils/find-parent-node.spec.ts +1 -5
- package/src/utils/find-parent-node.ts +1 -4
- package/src/utils/get-custom-selection.ts +1 -5
- package/src/utils/get-mark-type.ts +1 -4
- package/src/utils/get-node-type.ts +1 -4
- package/src/utils/get-node-types.ts +1 -4
- package/src/utils/includes-mark.ts +1 -5
- package/src/utils/is-at-block-start.ts +1 -4
- package/src/utils/is-mark-absent.spec.ts +1 -4
- package/src/utils/is-mark-absent.ts +1 -5
- package/src/utils/is-mark-active.ts +1 -4
- package/src/utils/is-node-active.spec.ts +109 -0
- package/src/utils/is-node-active.ts +17 -8
- package/src/utils/is-subset.spec.ts +1 -4
- package/src/utils/maybe-run.spec.ts +1 -5
- package/src/utils/merge-objects.spec.ts +1 -4
- package/src/utils/merge-objects.ts +2 -1
- package/src/utils/merge-specs.ts +1 -4
- package/src/utils/object-equal.spec.ts +1 -4
- package/src/utils/output-spec.test.ts +1 -5
- package/src/utils/output-spec.ts +12 -9
- package/src/utils/parse.spec.ts +2 -11
- package/src/utils/parse.ts +12 -24
- package/src/utils/remove-undefined-values.spec.ts +1 -4
- package/src/utils/set-selection-around.ts +1 -4
- package/src/utils/type-assertion.ts +2 -21
- package/src/utils/unicode.spec.ts +1 -4
- package/dist/editor-B0L9BgMi.js.map +0 -1
- package/dist/editor-M9OimMiI.d.ts.map +0 -1
- package/src/extensions/doc.ts +0 -31
- package/src/extensions/paragraph.ts +0 -61
- package/src/extensions/text.ts +0 -34
- package/src/types/base-node-view-options.ts +0 -33
- package/src/types/object-entries.ts +0 -13
- package/src/utils/collect-children.ts +0 -21
- package/src/utils/collect-nodes.ts +0 -37
- package/src/utils/deep-equals.spec.ts +0 -26
- package/src/utils/deep-equals.ts +0 -29
- package/src/utils/get-id.spec.ts +0 -14
- package/src/utils/get-id.ts +0 -13
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prosekit/core",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.10.0",
|
|
5
5
|
"private": false,
|
|
6
6
|
"description": "Core features for ProseKit",
|
|
7
7
|
"author": {
|
|
@@ -40,24 +40,21 @@
|
|
|
40
40
|
"src"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ocavue/utils": "^
|
|
43
|
+
"@ocavue/utils": "^1.4.0",
|
|
44
44
|
"clsx": "^2.1.1",
|
|
45
|
-
"just-clone": "^6.2.0",
|
|
46
|
-
"just-map-values": "^3.2.0",
|
|
47
45
|
"orderedmap": "^2.1.1",
|
|
48
46
|
"prosemirror-splittable": "^0.1.1",
|
|
49
|
-
"type-fest": "^5.
|
|
50
|
-
"@prosekit/pm": "^0.1.
|
|
47
|
+
"type-fest": "^5.4.3",
|
|
48
|
+
"@prosekit/pm": "^0.1.15"
|
|
51
49
|
},
|
|
52
50
|
"devDependencies": {
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"tsdown": "^0.16.5",
|
|
51
|
+
"diffable-html-snapshot": "^0.2.0",
|
|
52
|
+
"tsdown": "^0.20.1",
|
|
56
53
|
"typescript": "~5.9.3",
|
|
57
|
-
"vitest": "^4.0.
|
|
54
|
+
"vitest": "^4.0.18",
|
|
58
55
|
"vitest-browser-commands": "^0.2.0",
|
|
59
|
-
"@prosekit/config-
|
|
60
|
-
"@prosekit/config-
|
|
56
|
+
"@prosekit/config-vitest": "0.0.0",
|
|
57
|
+
"@prosekit/config-tsdown": "0.0.0"
|
|
61
58
|
},
|
|
62
59
|
"publishConfig": {
|
|
63
60
|
"dev": {}
|
package/src/commands/add-mark.ts
CHANGED
|
@@ -1,12 +1,5 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
MarkType,
|
|
4
|
-
ResolvedPos,
|
|
5
|
-
} from '@prosekit/pm/model'
|
|
6
|
-
import {
|
|
7
|
-
TextSelection,
|
|
8
|
-
type Command,
|
|
9
|
-
} from '@prosekit/pm/state'
|
|
1
|
+
import type { Mark, MarkType, ResolvedPos } from '@prosekit/pm/model'
|
|
2
|
+
import { TextSelection, type Command } from '@prosekit/pm/state'
|
|
10
3
|
|
|
11
4
|
import { getMarkType } from '../utils/get-mark-type'
|
|
12
5
|
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import type
|
|
2
|
-
Attrs,
|
|
3
|
-
NodeType,
|
|
4
|
-
ProseMirrorNode,
|
|
5
|
-
} from '@prosekit/pm/model'
|
|
1
|
+
import { Fragment, Slice, type Attrs, type NodeType, type ProseMirrorNode } from '@prosekit/pm/model'
|
|
6
2
|
import type { Command } from '@prosekit/pm/state'
|
|
7
|
-
import {
|
|
3
|
+
import { dropPoint } from '@prosekit/pm/transform'
|
|
8
4
|
|
|
9
5
|
import { assert } from '../utils/assert'
|
|
10
6
|
import { getNodeType } from '../utils/get-node-type'
|
|
@@ -52,10 +48,10 @@ function insertNode(options: InsertNodeOptions): Command {
|
|
|
52
48
|
|
|
53
49
|
assert(node, 'You must provide either a node or a type')
|
|
54
50
|
|
|
55
|
-
const insertPos =
|
|
51
|
+
const insertPos = dropPoint(
|
|
56
52
|
state.doc,
|
|
57
53
|
options.pos ?? state.selection.anchor,
|
|
58
|
-
node
|
|
54
|
+
new Slice(Fragment.from([node]), 0, 0),
|
|
59
55
|
)
|
|
60
56
|
if (insertPos == null) return false
|
|
61
57
|
|
|
@@ -14,8 +14,8 @@ export interface RemoveNodeOptions {
|
|
|
14
14
|
type: string | NodeType
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
* The document position to start searching node. By default it will
|
|
18
|
-
* anchor position of current selection.
|
|
17
|
+
* The document position to start searching for the node. By default, it will
|
|
18
|
+
* use the anchor position of current selection.
|
|
19
19
|
*/
|
|
20
20
|
pos?: number
|
|
21
21
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type Command,
|
|
4
|
-
} from '@prosekit/pm/state'
|
|
1
|
+
import { selectAll as selectAllCommand } from '@prosekit/pm/commands'
|
|
2
|
+
import type { Command } from '@prosekit/pm/state'
|
|
5
3
|
|
|
6
4
|
/**
|
|
7
5
|
* Returns a command that selects the whole document.
|
|
@@ -9,8 +7,5 @@ import {
|
|
|
9
7
|
* @public
|
|
10
8
|
*/
|
|
11
9
|
export function selectAll(): Command {
|
|
12
|
-
return
|
|
13
|
-
dispatch?.(state.tr.setSelection(new AllSelection(state.doc)))
|
|
14
|
-
return true
|
|
15
|
-
}
|
|
10
|
+
return selectAllCommand
|
|
16
11
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setupTest } from '../testing'
|
|
4
|
+
|
|
5
|
+
import { selectBlock } from './select-block'
|
|
6
|
+
|
|
7
|
+
describe('selectBlock', () => {
|
|
8
|
+
it('should expand the text selection to cover the start of the paragraph', () => {
|
|
9
|
+
const { editor, n, getSelectionString } = setup()
|
|
10
|
+
editor.set(n.doc(n.paragraph('Hello <a>world<b>')))
|
|
11
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("world")>"`)
|
|
12
|
+
editor.exec(selectBlock())
|
|
13
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello world")>"`)
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should expand the text selection to cover the end of the paragraph', () => {
|
|
17
|
+
const { editor, n, getSelectionString } = setup()
|
|
18
|
+
editor.set(n.doc(n.paragraph('<a>Hello<b> world')))
|
|
19
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello")>"`)
|
|
20
|
+
editor.exec(selectBlock())
|
|
21
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello world")>"`)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('should expand the text selection to include other marks', () => {
|
|
25
|
+
const { editor, n, m, getSelectionString } = setup()
|
|
26
|
+
editor.set(n.doc(n.paragraph(
|
|
27
|
+
m.bold('Bold'),
|
|
28
|
+
' ',
|
|
29
|
+
m.italic('Italic<a>'),
|
|
30
|
+
' ',
|
|
31
|
+
m.bold('Bold'),
|
|
32
|
+
)))
|
|
33
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<>"`)
|
|
34
|
+
editor.exec(selectBlock())
|
|
35
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph(bold("Bold"), " ", italic("Italic"), " ", bold("Bold"))>"`)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should expand the text selection to include multiple blocks', () => {
|
|
39
|
+
const { editor, n, getSelectionString } = setup()
|
|
40
|
+
editor.set(n.doc(
|
|
41
|
+
n.paragraph('Hello<a>'),
|
|
42
|
+
n.paragraph('World'),
|
|
43
|
+
n.paragraph('<b>!'),
|
|
44
|
+
))
|
|
45
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph, paragraph("World"), paragraph>"`)
|
|
46
|
+
editor.exec(selectBlock())
|
|
47
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph("Hello"), paragraph("World"), paragraph("!")>"`)
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('is able to expand the text selection pointing to different depths', () => {
|
|
51
|
+
const { editor, n, getSelectionString } = setup()
|
|
52
|
+
editor.set(n.doc(
|
|
53
|
+
n.paragraph('Hello<a>'),
|
|
54
|
+
n.blockquote([
|
|
55
|
+
n.paragraph('Item 1'),
|
|
56
|
+
n.blockquote([
|
|
57
|
+
n.paragraph('Sub<b> item 1.1'),
|
|
58
|
+
]),
|
|
59
|
+
]),
|
|
60
|
+
n.blockquote([
|
|
61
|
+
n.paragraph('Item 2'),
|
|
62
|
+
]),
|
|
63
|
+
))
|
|
64
|
+
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph, blockquote(paragraph("Item 1"), blockquote(paragraph("Sub")))>"`)
|
|
65
|
+
editor.exec(selectBlock())
|
|
66
|
+
expect(getSelectionString()).toMatchInlineSnapshot(
|
|
67
|
+
`"<paragraph("Hello"), blockquote(paragraph("Item 1"), blockquote(paragraph("Sub item 1.1")))>"`,
|
|
68
|
+
)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
function setup() {
|
|
73
|
+
const { editor, n, m } = setupTest()
|
|
74
|
+
|
|
75
|
+
const getSelectionString = () => {
|
|
76
|
+
const fragment = editor.state.selection.content().content
|
|
77
|
+
return fragment.toString()
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { editor, n, m, getSelectionString }
|
|
81
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { TextSelection, type Command } from '@prosekit/pm/state'
|
|
2
|
+
|
|
3
|
+
import { isTextSelection } from '../utils/type-assertion'
|
|
4
|
+
|
|
5
|
+
// Based on https://github.com/ProseMirror/prosemirror-commands/blob/1.7.1/src/commands.ts#L507-L521
|
|
6
|
+
function getTextblockEndpoint(selection: TextSelection, side: number): number | undefined {
|
|
7
|
+
const $pos = side < 0 ? selection.$from : selection.$to
|
|
8
|
+
let depth = $pos.depth
|
|
9
|
+
while ($pos.node(depth).isInline) {
|
|
10
|
+
if (!depth) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
depth--
|
|
14
|
+
}
|
|
15
|
+
if (!$pos.node(depth).isTextblock) {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
return side < 0 ? $pos.start(depth) : $pos.end(depth)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @internal
|
|
23
|
+
*/
|
|
24
|
+
export const selectBlockCommand: Command = (state, dispatch) => {
|
|
25
|
+
const { selection } = state
|
|
26
|
+
if (!isTextSelection(selection)) {
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const expectedFrom = getTextblockEndpoint(selection, -1)
|
|
31
|
+
const expectedTo = getTextblockEndpoint(selection, 1)
|
|
32
|
+
if (expectedFrom == null || expectedTo == null) {
|
|
33
|
+
return false
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (selection.from <= expectedFrom && selection.to >= expectedTo) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (dispatch) {
|
|
41
|
+
const newSelection = TextSelection.create(state.doc, expectedFrom, expectedTo)
|
|
42
|
+
dispatch(state.tr.setSelection(newSelection))
|
|
43
|
+
}
|
|
44
|
+
return true
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns a command to expand the text selection to cover the current block
|
|
49
|
+
* node. If the text selection spans multiple blocks, it will select all
|
|
50
|
+
* blocks in the selection.
|
|
51
|
+
*
|
|
52
|
+
* @public
|
|
53
|
+
*/
|
|
54
|
+
export function selectBlock(): Command {
|
|
55
|
+
return selectBlockCommand
|
|
56
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setupTest } from '../testing'
|
|
4
|
+
|
|
5
|
+
import { setNodeAttrsBetween } from './set-node-attrs-between'
|
|
6
|
+
|
|
7
|
+
describe('setNodeAttrsBetween', () => {
|
|
8
|
+
it('should set attributes on multiple nodes in selection range', () => {
|
|
9
|
+
const { editor, n } = setupTest()
|
|
10
|
+
|
|
11
|
+
editor.set(
|
|
12
|
+
n.doc(
|
|
13
|
+
n.codeBlock('<a>first block'),
|
|
14
|
+
n.codeBlock('second block<b>'),
|
|
15
|
+
n.codeBlock('third block'),
|
|
16
|
+
),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
const command = setNodeAttrsBetween({
|
|
20
|
+
type: 'codeBlock',
|
|
21
|
+
attrs: { language: 'rust' },
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
expect(editor.exec(command)).toBe(true)
|
|
25
|
+
|
|
26
|
+
expect(editor.state.doc.child(0).attrs.language).toBe('rust')
|
|
27
|
+
expect(editor.state.doc.child(1).attrs.language).toBe('rust')
|
|
28
|
+
expect(editor.state.doc.child(2).attrs.language).toBe('')
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it('should set attributes with explicit from/to positions', () => {
|
|
32
|
+
const { editor, n } = setupTest()
|
|
33
|
+
|
|
34
|
+
editor.set(
|
|
35
|
+
n.doc(
|
|
36
|
+
/*0*/
|
|
37
|
+
n.codeBlock(/*1*/ 'A' /*2*/),
|
|
38
|
+
/*3*/
|
|
39
|
+
n.codeBlock(/*4*/ 'B' /*5*/),
|
|
40
|
+
/*6*/
|
|
41
|
+
n.codeBlock(/*7*/ 'C' /*8*/),
|
|
42
|
+
/*9*/
|
|
43
|
+
),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
const command = setNodeAttrsBetween({
|
|
47
|
+
type: 'codeBlock',
|
|
48
|
+
attrs: { language: 'go' },
|
|
49
|
+
from: 5,
|
|
50
|
+
to: 7,
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
expect(editor.exec(command)).toBe(true)
|
|
54
|
+
|
|
55
|
+
expect(editor.state.doc.child(0).attrs.language).toBe('')
|
|
56
|
+
expect(editor.state.doc.child(1).attrs.language).toBe('go')
|
|
57
|
+
expect(editor.state.doc.child(2).attrs.language).toBe('go')
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it('should return false when no matching nodes in range', () => {
|
|
61
|
+
const { editor, n } = setupTest()
|
|
62
|
+
|
|
63
|
+
editor.set(
|
|
64
|
+
n.doc(
|
|
65
|
+
n.paragraph('<a>first paragraph'),
|
|
66
|
+
n.paragraph('second paragraph<b>'),
|
|
67
|
+
),
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
const command = setNodeAttrsBetween({
|
|
71
|
+
type: 'codeBlock',
|
|
72
|
+
attrs: { language: 'typescript' },
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
expect(editor.exec(command)).toBe(false)
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('should return false when from > to', () => {
|
|
79
|
+
const { editor, n } = setupTest()
|
|
80
|
+
|
|
81
|
+
editor.set(n.doc(n.codeBlock('code')))
|
|
82
|
+
|
|
83
|
+
const command = setNodeAttrsBetween({
|
|
84
|
+
type: 'codeBlock',
|
|
85
|
+
attrs: { language: 'typescript' },
|
|
86
|
+
from: 10,
|
|
87
|
+
to: 5,
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(editor.exec(command)).toBe(false)
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('should handle empty selection (from === to)', () => {
|
|
94
|
+
const { editor, n } = setupTest()
|
|
95
|
+
|
|
96
|
+
editor.set(n.doc(n.codeBlock('<a>code')))
|
|
97
|
+
|
|
98
|
+
const command = setNodeAttrsBetween({
|
|
99
|
+
type: 'codeBlock',
|
|
100
|
+
attrs: { language: 'typescript' },
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
expect(editor.exec(command)).toBe(true)
|
|
104
|
+
expect(editor.state.doc.firstChild?.attrs.language).toBe('typescript')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle partial node overlap', () => {
|
|
108
|
+
const { editor, n } = setupTest()
|
|
109
|
+
|
|
110
|
+
editor.set(
|
|
111
|
+
n.doc(
|
|
112
|
+
n.codeBlock('first bl<a>ock'),
|
|
113
|
+
n.codeBlock('second block'),
|
|
114
|
+
n.codeBlock('third bl<b>ock'),
|
|
115
|
+
),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
const command = setNodeAttrsBetween({
|
|
119
|
+
type: 'codeBlock',
|
|
120
|
+
attrs: { language: 'ruby' },
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
expect(editor.exec(command)).toBe(true)
|
|
124
|
+
|
|
125
|
+
expect(editor.state.doc.child(0).attrs.language).toBe('ruby')
|
|
126
|
+
expect(editor.state.doc.child(1).attrs.language).toBe('ruby')
|
|
127
|
+
expect(editor.state.doc.child(2).attrs.language).toBe('ruby')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('should update only matching node types in mixed content', () => {
|
|
131
|
+
const { editor, n } = setupTest()
|
|
132
|
+
|
|
133
|
+
editor.set(
|
|
134
|
+
n.doc(
|
|
135
|
+
n.paragraph('<a>paragraph 1'),
|
|
136
|
+
n.codeBlock('code block 1'),
|
|
137
|
+
n.paragraph('paragraph 2'),
|
|
138
|
+
n.codeBlock('code block 2'),
|
|
139
|
+
n.paragraph('paragraph 3<b>'),
|
|
140
|
+
),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
const command = setNodeAttrsBetween({
|
|
144
|
+
type: 'codeBlock',
|
|
145
|
+
attrs: { language: 'java' },
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
expect(editor.exec(command)).toBe(true)
|
|
149
|
+
|
|
150
|
+
expect(editor.state.doc.child(1).attrs.language).toBe('java')
|
|
151
|
+
expect(editor.state.doc.child(3).attrs.language).toBe('java')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
it('should handle nested nodes', () => {
|
|
155
|
+
const { editor, n } = setupTest()
|
|
156
|
+
|
|
157
|
+
const doc1 = n.doc(
|
|
158
|
+
n.blockquote(
|
|
159
|
+
{ variant: 'variant-0' },
|
|
160
|
+
n.paragraph('paragraph-0'),
|
|
161
|
+
),
|
|
162
|
+
n.blockquote(
|
|
163
|
+
{ variant: 'variant-1' },
|
|
164
|
+
n.paragraph('paragraph-1'),
|
|
165
|
+
n.blockquote(
|
|
166
|
+
{ variant: 'variant-1.1' },
|
|
167
|
+
n.paragraph('paragraph-1.1<a>'),
|
|
168
|
+
),
|
|
169
|
+
),
|
|
170
|
+
n.blockquote(
|
|
171
|
+
{ variant: 'variant-2' },
|
|
172
|
+
n.paragraph('paragraph-2'),
|
|
173
|
+
),
|
|
174
|
+
n.blockquote(
|
|
175
|
+
{ variant: 'variant-3' },
|
|
176
|
+
n.paragraph('paragraph-3<b>'),
|
|
177
|
+
n.blockquote(
|
|
178
|
+
{ variant: 'variant-3.1' },
|
|
179
|
+
n.paragraph('paragraph-3.1'),
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
const doc2 = n.doc(
|
|
185
|
+
n.blockquote(
|
|
186
|
+
{ variant: 'variant-0' },
|
|
187
|
+
n.paragraph('paragraph-0'),
|
|
188
|
+
),
|
|
189
|
+
n.blockquote(
|
|
190
|
+
{ variant: 'variant-X' },
|
|
191
|
+
n.paragraph('paragraph-1'),
|
|
192
|
+
n.blockquote(
|
|
193
|
+
{ variant: 'variant-X' },
|
|
194
|
+
n.paragraph('paragraph-1.1'),
|
|
195
|
+
),
|
|
196
|
+
),
|
|
197
|
+
n.blockquote(
|
|
198
|
+
{ variant: 'variant-X' },
|
|
199
|
+
n.paragraph('paragraph-2'),
|
|
200
|
+
),
|
|
201
|
+
n.blockquote(
|
|
202
|
+
{ variant: 'variant-X' },
|
|
203
|
+
n.paragraph('paragraph-3'),
|
|
204
|
+
n.blockquote(
|
|
205
|
+
{ variant: 'variant-3.1' },
|
|
206
|
+
n.paragraph('paragraph-3.1'),
|
|
207
|
+
),
|
|
208
|
+
),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
editor.set(doc1)
|
|
212
|
+
|
|
213
|
+
const command = setNodeAttrsBetween({
|
|
214
|
+
type: 'blockquote',
|
|
215
|
+
attrs: { variant: 'variant-X' },
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
expect(editor.exec(command)).toBe(true)
|
|
219
|
+
expect(editor.getDocJSON()).toEqual(doc2.toJSON())
|
|
220
|
+
})
|
|
221
|
+
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { Attrs, NodeType } from '@prosekit/pm/model'
|
|
2
|
+
import type { Command } from '@prosekit/pm/state'
|
|
3
|
+
|
|
4
|
+
import { getNodeTypes } from '../utils/get-node-types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @public
|
|
8
|
+
*/
|
|
9
|
+
export interface SetNodeAttrsBetweenOptions {
|
|
10
|
+
/**
|
|
11
|
+
* The type of node to set the attributes of.
|
|
12
|
+
*/
|
|
13
|
+
type: string | NodeType | string[] | NodeType[]
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The attributes to set.
|
|
17
|
+
*/
|
|
18
|
+
attrs: Attrs
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* The position to start searching for nodes. By default, the selection from position will be used.
|
|
22
|
+
*/
|
|
23
|
+
from?: number
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The position to end searching for nodes. By default, the selection to position will be used.
|
|
27
|
+
*/
|
|
28
|
+
to?: number
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Returns a command that sets the attributes of all matching nodes between the
|
|
33
|
+
* `from` and `to` positions.
|
|
34
|
+
*
|
|
35
|
+
* @param options
|
|
36
|
+
*
|
|
37
|
+
* @public
|
|
38
|
+
*/
|
|
39
|
+
export function setNodeAttrsBetween(options: SetNodeAttrsBetweenOptions): Command {
|
|
40
|
+
return (state, dispatch) => {
|
|
41
|
+
const from = options.from ?? state.selection.from
|
|
42
|
+
const to = options.to ?? state.selection.to
|
|
43
|
+
if (from > to) {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const nodeTypes = getNodeTypes(state.schema, options.type)
|
|
48
|
+
const positions: number[] = []
|
|
49
|
+
let found = false
|
|
50
|
+
|
|
51
|
+
state.doc.nodesBetween(from, to, (node, pos) => {
|
|
52
|
+
if (nodeTypes.includes(node.type)) {
|
|
53
|
+
positions.push(pos)
|
|
54
|
+
found = true
|
|
55
|
+
}
|
|
56
|
+
if (!dispatch && found) {
|
|
57
|
+
// Early return to prevent further iteration
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
if (!found) {
|
|
63
|
+
return false
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (dispatch) {
|
|
67
|
+
const { tr } = state
|
|
68
|
+
for (const [key, value] of Object.entries(options.attrs)) {
|
|
69
|
+
for (const pos of positions) {
|
|
70
|
+
tr.setNodeAttribute(pos, key, value)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
dispatch(tr)
|
|
74
|
+
}
|
|
75
|
+
return true
|
|
76
|
+
}
|
|
77
|
+
}
|