@prosekit/core 0.9.0 → 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-4lgGc3CY.d.ts → editor-BULC1zqX.d.ts} +12 -12
- package/dist/editor-BULC1zqX.d.ts.map +1 -0
- package/dist/{editor-DGNUXn-u.js → editor-g-Rqn-ZE.js} +30 -30
- 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 +103 -24
- package/dist/prosekit-core.d.ts.map +1 -1
- package/dist/prosekit-core.js +116 -35
- package/dist/prosekit-core.js.map +1 -1
- package/package.json +6 -7
- 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-block.spec.ts +4 -6
- package/src/commands/select-block.ts +1 -4
- 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 +1 -4
- package/src/editor/action.spec.ts +2 -6
- package/src/editor/action.ts +1 -8
- package/src/editor/editor.spec.ts +2 -9
- package/src/editor/editor.ts +11 -45
- 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 +16 -48
- package/src/extensions/default-state.spec.ts +1 -5
- package/src/extensions/default-state.ts +2 -8
- package/src/extensions/events/dom-event.spec.ts +1 -6
- package/src/extensions/events/dom-event.ts +4 -16
- package/src/extensions/events/editor-event.ts +4 -16
- package/src/extensions/events/focus.spec.ts +1 -6
- package/src/extensions/events/plugin-view.ts +2 -9
- package/src/extensions/history.ts +2 -9
- package/src/extensions/keymap-base.spec.ts +2 -11
- package/src/extensions/keymap-base.ts +1 -4
- package/src/extensions/keymap.spec.ts +6 -20
- package/src/extensions/keymap.ts +3 -13
- package/src/extensions/mark-spec.spec.ts +5 -20
- package/src/extensions/mark-spec.ts +4 -15
- 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 +4 -15
- 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 +3 -12
- package/src/facets/base-extension.ts +1 -4
- package/src/facets/command.ts +2 -8
- 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 +2 -9
- package/src/facets/state.ts +4 -18
- package/src/facets/union-extension.ts +2 -8
- package/src/index.ts +40 -166
- 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.spec.ts +1 -4
- package/src/types/extension.ts +3 -13
- package/src/types/simplify-union.ts +1 -4
- package/src/utils/array-grouping.spec.ts +1 -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/default-block-at.ts +1 -4
- package/src/utils/editor-content.spec.ts +1 -4
- package/src/utils/editor-content.ts +4 -16
- 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 +2 -10
- package/src/utils/parse.spec.ts +2 -11
- package/src/utils/parse.ts +3 -15
- 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-4lgGc3CY.d.ts.map +0 -1
- package/dist/editor-DGNUXn-u.js.map +0 -1
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,19 +40,18 @@
|
|
|
40
40
|
"src"
|
|
41
41
|
],
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@ocavue/utils": "^1.
|
|
43
|
+
"@ocavue/utils": "^1.4.0",
|
|
44
44
|
"clsx": "^2.1.1",
|
|
45
45
|
"orderedmap": "^2.1.1",
|
|
46
46
|
"prosemirror-splittable": "^0.1.1",
|
|
47
|
-
"type-fest": "^5.3
|
|
47
|
+
"type-fest": "^5.4.3",
|
|
48
48
|
"@prosekit/pm": "^0.1.15"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"tsdown": "^0.17.0",
|
|
51
|
+
"diffable-html-snapshot": "^0.2.0",
|
|
52
|
+
"tsdown": "^0.20.1",
|
|
54
53
|
"typescript": "~5.9.3",
|
|
55
|
-
"vitest": "^4.0.
|
|
54
|
+
"vitest": "^4.0.18",
|
|
56
55
|
"vitest-browser-commands": "^0.2.0",
|
|
57
56
|
"@prosekit/config-vitest": "0.0.0",
|
|
58
57
|
"@prosekit/config-tsdown": "0.0.0"
|
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,8 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
describe,
|
|
3
|
-
expect,
|
|
4
|
-
it,
|
|
5
|
-
} from 'vitest'
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
6
2
|
|
|
7
3
|
import { setupTest } from '../testing'
|
|
8
4
|
|
|
@@ -67,7 +63,9 @@ describe('selectBlock', () => {
|
|
|
67
63
|
))
|
|
68
64
|
expect(getSelectionString()).toMatchInlineSnapshot(`"<paragraph, blockquote(paragraph("Item 1"), blockquote(paragraph("Sub")))>"`)
|
|
69
65
|
editor.exec(selectBlock())
|
|
70
|
-
expect(getSelectionString()).toMatchInlineSnapshot(
|
|
66
|
+
expect(getSelectionString()).toMatchInlineSnapshot(
|
|
67
|
+
`"<paragraph("Hello"), blockquote(paragraph("Item 1"), blockquote(paragraph("Sub item 1.1")))>"`,
|
|
68
|
+
)
|
|
71
69
|
})
|
|
72
70
|
})
|
|
73
71
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
|
|
3
|
+
import { setupTest } from '../testing'
|
|
4
|
+
|
|
5
|
+
import { setNodeAttrs } from './set-node-attrs'
|
|
6
|
+
|
|
7
|
+
describe('setNodeAttrs', () => {
|
|
8
|
+
it('should set attributes on a code block node', () => {
|
|
9
|
+
const { editor, n } = setupTest()
|
|
10
|
+
|
|
11
|
+
editor.set(n.doc(n.codeBlock('const x = 1')))
|
|
12
|
+
|
|
13
|
+
// Get the initial language attribute (should be empty string by default)
|
|
14
|
+
expect(editor.state.doc.firstChild?.attrs.language).toBe('')
|
|
15
|
+
|
|
16
|
+
// Set the language attribute
|
|
17
|
+
const command = setNodeAttrs({
|
|
18
|
+
type: 'codeBlock',
|
|
19
|
+
attrs: { language: 'typescript' },
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
expect(editor.exec(command)).toBe(true)
|
|
23
|
+
expect(editor.state.doc.firstChild?.attrs.language).toBe('typescript')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('should set multiple attributes at once', () => {
|
|
27
|
+
const { editor, n } = setupTest()
|
|
28
|
+
|
|
29
|
+
editor.set(n.doc(n.codeBlock('const x = 1')))
|
|
30
|
+
|
|
31
|
+
// Verify initial state
|
|
32
|
+
expect(editor.state.doc.firstChild?.attrs).toMatchObject({
|
|
33
|
+
language: '',
|
|
34
|
+
lineNumbers: false,
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
// Set multiple attributes at once
|
|
38
|
+
const command = setNodeAttrs({
|
|
39
|
+
type: 'codeBlock',
|
|
40
|
+
attrs: { language: 'javascript', lineNumbers: true },
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
editor.exec(command)
|
|
44
|
+
|
|
45
|
+
// Verify both attributes were set
|
|
46
|
+
expect(editor.state.doc.firstChild?.attrs).toMatchObject({
|
|
47
|
+
language: 'javascript',
|
|
48
|
+
lineNumbers: true,
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('should return false when node type does not match', () => {
|
|
53
|
+
const { editor, n } = setupTest()
|
|
54
|
+
|
|
55
|
+
editor.set(n.doc(n.paragraph('Hello world')))
|
|
56
|
+
|
|
57
|
+
const command = setNodeAttrs({
|
|
58
|
+
type: 'codeBlock',
|
|
59
|
+
attrs: { language: 'typescript' },
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// Should return false because paragraph is not a codeBlock
|
|
63
|
+
expect(editor.exec(command)).toBe(false)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('should set attributes at a specific position', () => {
|
|
67
|
+
const { editor, n } = setupTest()
|
|
68
|
+
|
|
69
|
+
editor.set(
|
|
70
|
+
n.doc(
|
|
71
|
+
/*0*/
|
|
72
|
+
n.codeBlock(/*1*/ 'A' /*2*/),
|
|
73
|
+
/*3*/
|
|
74
|
+
n.codeBlock(/*4*/ 'B' /*5*/),
|
|
75
|
+
/*6*/
|
|
76
|
+
),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
// Set attribute on the second code block (position after first block)
|
|
80
|
+
const command = setNodeAttrs({
|
|
81
|
+
type: 'codeBlock',
|
|
82
|
+
attrs: { language: 'python' },
|
|
83
|
+
pos: 3, // Position of second code block
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
editor.exec(command)
|
|
87
|
+
|
|
88
|
+
// First block should still have default language
|
|
89
|
+
expect(editor.state.doc.child(0).attrs.language).toBe('')
|
|
90
|
+
// Second block should have the new language
|
|
91
|
+
expect(editor.state.doc.child(1).attrs.language).toBe('python')
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('should handle cursor inside a node', () => {
|
|
95
|
+
const { editor, n } = setupTest()
|
|
96
|
+
|
|
97
|
+
editor.set(n.doc(n.codeBlock('const<a> x = 1')))
|
|
98
|
+
|
|
99
|
+
const command = setNodeAttrs({
|
|
100
|
+
type: 'codeBlock',
|
|
101
|
+
attrs: { language: 'typescript' },
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
editor.exec(command)
|
|
105
|
+
|
|
106
|
+
expect(editor.state.doc.firstChild?.attrs.language).toBe('typescript')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should set attrs on wrapping node containing selection', () => {
|
|
110
|
+
const { editor, n } = setupTest()
|
|
111
|
+
|
|
112
|
+
editor.set(
|
|
113
|
+
n.doc(
|
|
114
|
+
n.blockquote(
|
|
115
|
+
n.paragraph('Hello<a> world<b>'),
|
|
116
|
+
),
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
const command = setNodeAttrs({
|
|
121
|
+
type: 'blockquote',
|
|
122
|
+
attrs: { variant: 'fancy' },
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
// Should find the blockquote wrapping the selection
|
|
126
|
+
expect(editor.exec(command)).toBe(true)
|
|
127
|
+
expect(editor.state.doc.firstChild?.attrs.variant).toBe('fancy')
|
|
128
|
+
})
|
|
129
|
+
})
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
Attrs,
|
|
3
|
-
NodeType,
|
|
4
|
-
} from '@prosekit/pm/model'
|
|
1
|
+
import type { Attrs, NodeType } from '@prosekit/pm/model'
|
|
5
2
|
import type { Command } from '@prosekit/pm/state'
|
|
6
3
|
|
|
4
|
+
import { findParentNodeOfType } from '../utils/find-parent-node-of-type'
|
|
7
5
|
import { getNodeTypes } from '../utils/get-node-types'
|
|
8
6
|
|
|
9
7
|
/**
|
|
@@ -12,8 +10,6 @@ import { getNodeTypes } from '../utils/get-node-types'
|
|
|
12
10
|
export interface SetNodeAttrsOptions {
|
|
13
11
|
/**
|
|
14
12
|
* 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
13
|
*/
|
|
18
14
|
type: string | NodeType | string[] | NodeType[]
|
|
19
15
|
|
|
@@ -23,43 +19,46 @@ export interface SetNodeAttrsOptions {
|
|
|
23
19
|
attrs: Attrs
|
|
24
20
|
|
|
25
21
|
/**
|
|
26
|
-
* The position of the node
|
|
27
|
-
*
|
|
22
|
+
* The document position of the node to update. If not provided, the command
|
|
23
|
+
* will find the closest ancestor node that matches the type based on the
|
|
24
|
+
* anchor position of the selection.
|
|
28
25
|
*/
|
|
29
26
|
pos?: number
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
/**
|
|
33
|
-
* Returns a command that
|
|
30
|
+
* Returns a command that sets the attributes of the current node.
|
|
31
|
+
*
|
|
32
|
+
* @param options
|
|
34
33
|
*
|
|
35
34
|
* @public
|
|
36
35
|
*/
|
|
37
|
-
export function setNodeAttrs(
|
|
36
|
+
export function setNodeAttrs({ type, attrs, pos }: SetNodeAttrsOptions): Command {
|
|
38
37
|
return (state, dispatch) => {
|
|
39
|
-
|
|
40
|
-
const from = options.pos ?? state.selection.from
|
|
41
|
-
const to = options.pos ?? state.selection.to
|
|
42
|
-
const positions: number[] = []
|
|
38
|
+
let updatePos: number
|
|
43
39
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
if (pos == null) {
|
|
41
|
+
const found = findParentNodeOfType(type, state.selection.$anchor)
|
|
42
|
+
if (!found) {
|
|
43
|
+
return false
|
|
47
44
|
}
|
|
48
|
-
|
|
45
|
+
updatePos = found.pos
|
|
46
|
+
} else {
|
|
47
|
+
const found = state.doc.nodeAt(pos)
|
|
48
|
+
if (!found) {
|
|
49
49
|
return false
|
|
50
50
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
51
|
+
const nodeTypes = getNodeTypes(state.schema, type)
|
|
52
|
+
if (!nodeTypes.includes(found.type)) {
|
|
53
|
+
return false
|
|
54
|
+
}
|
|
55
|
+
updatePos = pos
|
|
55
56
|
}
|
|
56
57
|
|
|
57
58
|
if (dispatch) {
|
|
58
59
|
const { tr } = state
|
|
59
|
-
for (const
|
|
60
|
-
|
|
61
|
-
tr.setNodeAttribute(pos, key, value)
|
|
62
|
-
}
|
|
60
|
+
for (const [key, value] of Object.entries(attrs)) {
|
|
61
|
+
tr.setNodeAttribute(updatePos, key, value)
|
|
63
62
|
}
|
|
64
63
|
dispatch(tr)
|
|
65
64
|
}
|