@portabletext/editor 2.15.0 → 2.15.2
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/lib/index.cjs +108 -24
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +106 -24
- package/lib/index.js.map +1 -1
- package/lib/utils/index.d.cts +2 -2
- package/package.json +9 -9
- package/src/editor/plugins/createWithObjectKeys.ts +61 -6
- package/src/internal-utils/applyPatch.ts +13 -3
- package/src/internal-utils/operation-to-patches.ts +49 -14
- package/src/internal-utils/text-selection.test.ts +13 -0
- package/src/internal-utils/text-selection.ts +1 -0
- package/src/operations/behavior.operation.block.set.ts +3 -7
- package/src/operations/behavior.operation.insert.child.ts +47 -21
- package/src/test/vitest/step-context.ts +2 -0
- package/src/test/vitest/step-definitions.tsx +62 -18
- package/src/test/vitest/test-editor.tsx +94 -0
package/lib/utils/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BlockOffset, BlockPath, ChildPath, EditorContext, EditorSelection, EditorSelectionPoint } from "../_chunks-dts/behavior.types.action.cjs";
|
|
2
|
-
import * as
|
|
2
|
+
import * as _sanity_types8 from "@sanity/types";
|
|
3
3
|
import { KeyedSegment, PortableTextBlock, PortableTextTextBlock } from "@sanity/types";
|
|
4
4
|
import { isSpan, isTextBlock } from "@portabletext/schema";
|
|
5
5
|
/**
|
|
@@ -143,7 +143,7 @@ declare function mergeTextBlocks({
|
|
|
143
143
|
context: Pick<EditorContext, 'keyGenerator' | 'schema'>;
|
|
144
144
|
targetBlock: PortableTextTextBlock;
|
|
145
145
|
incomingBlock: PortableTextTextBlock;
|
|
146
|
-
}): PortableTextTextBlock<
|
|
146
|
+
}): PortableTextTextBlock<_sanity_types8.PortableTextObject | _sanity_types8.PortableTextSpan>;
|
|
147
147
|
/**
|
|
148
148
|
* @public
|
|
149
149
|
*/
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "2.15.
|
|
3
|
+
"version": "2.15.2",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"slate-dom": "^0.118.1",
|
|
86
86
|
"slate-react": "0.117.4",
|
|
87
87
|
"xstate": "^5.23.0",
|
|
88
|
-
"@portabletext/block-tools": "^3.5.
|
|
88
|
+
"@portabletext/block-tools": "^3.5.12",
|
|
89
89
|
"@portabletext/keyboard-shortcuts": "^1.1.1",
|
|
90
90
|
"@portabletext/patches": "^1.1.8",
|
|
91
91
|
"@portabletext/schema": "^1.2.0"
|
|
@@ -93,8 +93,8 @@
|
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
95
95
|
"@sanity/pkg-utils": "^8.1.14",
|
|
96
|
-
"@sanity/schema": "^4.
|
|
97
|
-
"@sanity/types": "^4.
|
|
96
|
+
"@sanity/schema": "^4.11.0",
|
|
97
|
+
"@sanity/types": "^4.11.0",
|
|
98
98
|
"@types/debug": "^4.1.12",
|
|
99
99
|
"@types/lodash": "^4.17.20",
|
|
100
100
|
"@types/lodash.startcase": "^4.4.9",
|
|
@@ -114,17 +114,17 @@
|
|
|
114
114
|
"rxjs": "^7.8.2",
|
|
115
115
|
"typescript": "5.9.3",
|
|
116
116
|
"typescript-eslint": "^8.46.1",
|
|
117
|
-
"vite": "^7.1.
|
|
117
|
+
"vite": "^7.1.10",
|
|
118
118
|
"vitest": "^3.2.4",
|
|
119
119
|
"vitest-browser-react": "^1.0.1",
|
|
120
|
-
"@portabletext/sanity-bridge": "1.1.
|
|
120
|
+
"@portabletext/sanity-bridge": "1.1.15",
|
|
121
121
|
"@portabletext/test": "^0.0.0",
|
|
122
122
|
"racejar": "1.3.2"
|
|
123
123
|
},
|
|
124
124
|
"peerDependencies": {
|
|
125
|
-
"@portabletext/sanity-bridge": "^1.1.
|
|
126
|
-
"@sanity/schema": "^4.
|
|
127
|
-
"@sanity/types": "^4.
|
|
125
|
+
"@portabletext/sanity-bridge": "^1.1.15",
|
|
126
|
+
"@sanity/schema": "^4.11.0",
|
|
127
|
+
"@sanity/types": "^4.11.0",
|
|
128
128
|
"react": "^18.3 || ^19",
|
|
129
129
|
"rxjs": "^7.8.2"
|
|
130
130
|
},
|
|
@@ -193,6 +193,43 @@ export function createWithObjectKeys(editorActor: EditorActor) {
|
|
|
193
193
|
|
|
194
194
|
editor.normalizeNode = (entry) => {
|
|
195
195
|
const [node, path] = entry
|
|
196
|
+
|
|
197
|
+
if (Element.isElement(node)) {
|
|
198
|
+
const [parent] = Editor.parent(editor, path)
|
|
199
|
+
|
|
200
|
+
if (parent && Editor.isEditor(parent)) {
|
|
201
|
+
const blockKeys = new Set<string>()
|
|
202
|
+
|
|
203
|
+
for (const sibling of parent.children) {
|
|
204
|
+
if (sibling._key && blockKeys.has(sibling._key)) {
|
|
205
|
+
const _key = editorActor.getSnapshot().context.keyGenerator()
|
|
206
|
+
|
|
207
|
+
blockKeys.add(_key)
|
|
208
|
+
|
|
209
|
+
withNormalizeNode(editor, () => {
|
|
210
|
+
Transforms.setNodes(editor, {_key}, {at: path})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
return
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (!sibling._key) {
|
|
217
|
+
const _key = editorActor.getSnapshot().context.keyGenerator()
|
|
218
|
+
|
|
219
|
+
blockKeys.add(_key)
|
|
220
|
+
|
|
221
|
+
withNormalizeNode(editor, () => {
|
|
222
|
+
Transforms.setNodes(editor, {_key}, {at: path})
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
blockKeys.add(sibling._key)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
196
233
|
if (
|
|
197
234
|
Element.isElement(node) &&
|
|
198
235
|
node._type === editorActor.getSnapshot().context.schema.block.name
|
|
@@ -208,18 +245,36 @@ export function createWithObjectKeys(editorActor: EditorActor) {
|
|
|
208
245
|
})
|
|
209
246
|
return
|
|
210
247
|
}
|
|
211
|
-
|
|
248
|
+
|
|
249
|
+
// Set unique keys on it's children
|
|
250
|
+
const childKeys = new Set<string>()
|
|
251
|
+
|
|
212
252
|
for (const [child, childPath] of Node.children(editor, path)) {
|
|
253
|
+
if (child._key && childKeys.has(child._key)) {
|
|
254
|
+
const _key = editorActor.getSnapshot().context.keyGenerator()
|
|
255
|
+
|
|
256
|
+
childKeys.add(_key)
|
|
257
|
+
|
|
258
|
+
withNormalizeNode(editor, () => {
|
|
259
|
+
Transforms.setNodes(editor, {_key}, {at: childPath})
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
213
265
|
if (!child._key) {
|
|
266
|
+
const _key = editorActor.getSnapshot().context.keyGenerator()
|
|
267
|
+
|
|
268
|
+
childKeys.add(_key)
|
|
269
|
+
|
|
214
270
|
withNormalizeNode(editor, () => {
|
|
215
|
-
Transforms.setNodes(
|
|
216
|
-
editor,
|
|
217
|
-
{_key: editorActor.getSnapshot().context.keyGenerator()},
|
|
218
|
-
{at: childPath},
|
|
219
|
-
)
|
|
271
|
+
Transforms.setNodes(editor, {_key}, {at: childPath})
|
|
220
272
|
})
|
|
273
|
+
|
|
221
274
|
return
|
|
222
275
|
}
|
|
276
|
+
|
|
277
|
+
childKeys.add(child._key)
|
|
223
278
|
}
|
|
224
279
|
}
|
|
225
280
|
|
|
@@ -202,9 +202,19 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
|
|
|
202
202
|
|
|
203
203
|
const isTextBlock = editor.isTextBlock(block.node)
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
if (isTextBlock && patch.path[1] !== 'children') {
|
|
206
|
+
const updatedBlock = applyAll(block.node, [
|
|
207
|
+
{
|
|
208
|
+
...patch,
|
|
209
|
+
path: patch.path.slice(1),
|
|
210
|
+
},
|
|
211
|
+
])
|
|
212
|
+
|
|
213
|
+
Transforms.setNodes(editor, updatedBlock as Partial<Node>, {
|
|
214
|
+
at: [block.index],
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
return true
|
|
208
218
|
}
|
|
209
219
|
|
|
210
220
|
const child = findBlockChild(block, patch.path)
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
} from '@portabletext/patches'
|
|
10
10
|
import {isSpan, isTextBlock} from '@portabletext/schema'
|
|
11
11
|
import type {Path, PortableTextSpan, PortableTextTextBlock} from '@sanity/types'
|
|
12
|
-
import {get
|
|
12
|
+
import {get} from 'lodash'
|
|
13
13
|
import {
|
|
14
14
|
Element,
|
|
15
15
|
Text,
|
|
@@ -101,20 +101,55 @@ export function setNodePatch(
|
|
|
101
101
|
children: Descendant[],
|
|
102
102
|
operation: SetNodeOperation,
|
|
103
103
|
): Array<Patch> {
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
const blockIndex = operation.path.at(0)
|
|
105
|
+
|
|
106
|
+
if (blockIndex !== undefined && operation.path.length === 1) {
|
|
107
|
+
const block = children.at(blockIndex)
|
|
108
|
+
|
|
109
|
+
if (!block) {
|
|
110
|
+
console.error('Could not find block at index', blockIndex)
|
|
111
|
+
return []
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (isTextBlock({schema}, block)) {
|
|
115
|
+
const patches: Patch[] = []
|
|
116
|
+
|
|
117
|
+
for (const key of Object.keys(operation.newProperties)) {
|
|
118
|
+
const value = (operation.newProperties as Record<string, unknown>)[key]
|
|
119
|
+
|
|
120
|
+
if (key === '_key') {
|
|
121
|
+
patches.push(set(value, [blockIndex, '_key']))
|
|
122
|
+
} else {
|
|
123
|
+
patches.push(set(value, [{_key: block._key}, key]))
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return patches
|
|
128
|
+
} else {
|
|
129
|
+
const patches: Patch[] = []
|
|
130
|
+
|
|
131
|
+
const _key = operation.newProperties._key
|
|
132
|
+
|
|
133
|
+
if (_key !== undefined) {
|
|
134
|
+
patches.push(set(_key, [blockIndex, '_key']))
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const properties =
|
|
138
|
+
'value' in operation.newProperties &&
|
|
139
|
+
typeof operation.newProperties.value === 'object'
|
|
140
|
+
? (operation.newProperties.value as Record<string, unknown>)
|
|
141
|
+
: ({} satisfies Record<string, unknown>)
|
|
142
|
+
|
|
143
|
+
const keys = Object.keys(properties)
|
|
144
|
+
|
|
145
|
+
for (const key of keys) {
|
|
146
|
+
const value = properties[key]
|
|
147
|
+
|
|
148
|
+
patches.push(set(value, [{_key: block._key}, key]))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return patches
|
|
108
152
|
}
|
|
109
|
-
const setNode = omitBy(
|
|
110
|
-
{...children[operation.path[0]], ...operation.newProperties},
|
|
111
|
-
isUndefined,
|
|
112
|
-
) as unknown as Descendant
|
|
113
|
-
return [
|
|
114
|
-
set(fromSlateValue([setNode], schema.block.name)[0], [
|
|
115
|
-
{_key: block._key},
|
|
116
|
-
]),
|
|
117
|
-
]
|
|
118
153
|
} else if (operation.path.length === 2) {
|
|
119
154
|
const block = children[operation.path[0]]
|
|
120
155
|
if (isTextBlock({schema}, block)) {
|
|
@@ -17,6 +17,7 @@ test(getTextSelection.name, () => {
|
|
|
17
17
|
expect(getTextSelection({schema, value: [simpleBlock]}, 'foo')).toEqual({
|
|
18
18
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
|
|
19
19
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 3},
|
|
20
|
+
backward: false,
|
|
20
21
|
})
|
|
21
22
|
|
|
22
23
|
const joinedBlock = {
|
|
@@ -28,18 +29,22 @@ test(getTextSelection.name, () => {
|
|
|
28
29
|
expect(getTextSelection({schema, value: [joinedBlock]}, 'foo ')).toEqual({
|
|
29
30
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
|
|
30
31
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
|
|
32
|
+
backward: false,
|
|
31
33
|
})
|
|
32
34
|
expect(getTextSelection({schema, value: [joinedBlock]}, 'o')).toEqual({
|
|
33
35
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
|
|
34
36
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
|
|
37
|
+
backward: false,
|
|
35
38
|
})
|
|
36
39
|
expect(getTextSelection({schema, value: [joinedBlock]}, 'bar')).toEqual({
|
|
37
40
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 4},
|
|
38
41
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 7},
|
|
42
|
+
backward: false,
|
|
39
43
|
})
|
|
40
44
|
expect(getTextSelection({schema, value: [joinedBlock]}, ' baz')).toEqual({
|
|
41
45
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 7},
|
|
42
46
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 11},
|
|
47
|
+
backward: false,
|
|
43
48
|
})
|
|
44
49
|
|
|
45
50
|
const noSpaceBlock = {
|
|
@@ -54,6 +59,7 @@ test(getTextSelection.name, () => {
|
|
|
54
59
|
expect(getTextSelection({schema, value: [noSpaceBlock]}, 'obar')).toEqual({
|
|
55
60
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
|
|
56
61
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
|
|
62
|
+
backward: false,
|
|
57
63
|
})
|
|
58
64
|
|
|
59
65
|
const emptyLineBlock = {
|
|
@@ -70,6 +76,7 @@ test(getTextSelection.name, () => {
|
|
|
70
76
|
{
|
|
71
77
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
|
|
72
78
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 3},
|
|
79
|
+
backward: false,
|
|
73
80
|
},
|
|
74
81
|
)
|
|
75
82
|
|
|
@@ -86,24 +93,29 @@ test(getTextSelection.name, () => {
|
|
|
86
93
|
expect(getTextSelection({schema, value: [splitBlock]}, 'foo')).toEqual({
|
|
87
94
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
|
|
88
95
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 3},
|
|
96
|
+
backward: false,
|
|
89
97
|
})
|
|
90
98
|
expect(getTextSelection({schema, value: [splitBlock]}, 'bar')).toEqual({
|
|
91
99
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 0},
|
|
92
100
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's2'}], offset: 3},
|
|
101
|
+
backward: false,
|
|
93
102
|
})
|
|
94
103
|
expect(getTextSelection({schema, value: [splitBlock]}, 'baz')).toEqual({
|
|
95
104
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 1},
|
|
96
105
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
|
|
106
|
+
backward: false,
|
|
97
107
|
})
|
|
98
108
|
expect(
|
|
99
109
|
getTextSelection({schema, value: [splitBlock]}, 'foo bar baz'),
|
|
100
110
|
).toEqual({
|
|
101
111
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 0},
|
|
102
112
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 4},
|
|
113
|
+
backward: false,
|
|
103
114
|
})
|
|
104
115
|
expect(getTextSelection({schema, value: [splitBlock]}, 'o bar b')).toEqual({
|
|
105
116
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 2},
|
|
106
117
|
focus: {path: [{_key: 'b1'}, 'children', {_key: 's3'}], offset: 2},
|
|
118
|
+
backward: false,
|
|
107
119
|
})
|
|
108
120
|
|
|
109
121
|
const twoBlocks = [
|
|
@@ -122,6 +134,7 @@ test(getTextSelection.name, () => {
|
|
|
122
134
|
expect(getTextSelection({schema, value: twoBlocks}, 'ooba')).toEqual({
|
|
123
135
|
anchor: {path: [{_key: 'b1'}, 'children', {_key: 's1'}], offset: 1},
|
|
124
136
|
focus: {path: [{_key: 'b2'}, 'children', {_key: 's2'}], offset: 2},
|
|
137
|
+
backward: false,
|
|
125
138
|
})
|
|
126
139
|
})
|
|
127
140
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {Transforms, type Element as SlateElement} from 'slate'
|
|
2
|
-
import {
|
|
2
|
+
import {toSlateBlock} from '../internal-utils/values'
|
|
3
3
|
import {parseBlock} from '../utils/parse-blocks'
|
|
4
4
|
import type {BehaviorOperationImplementation} from './behavior.operations'
|
|
5
5
|
|
|
@@ -40,13 +40,9 @@ export const blockSetOperationImplementation: BehaviorOperationImplementation<
|
|
|
40
40
|
throw new Error(`Unable to update block at ${JSON.stringify(operation.at)}`)
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
const slateBlock =
|
|
43
|
+
const slateBlock = toSlateBlock(parsedBlock, {
|
|
44
44
|
schemaTypes: context.schema,
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
if (!slateBlock) {
|
|
48
|
-
throw new Error(`Unable to convert block to Slate value`)
|
|
49
|
-
}
|
|
45
|
+
}) as SlateElement
|
|
50
46
|
|
|
51
47
|
Transforms.setNodes(operation.editor, slateBlock, {at: [blockIndex]})
|
|
52
48
|
}
|
|
@@ -74,27 +74,53 @@ export const insertChildOperationImplementation: BehaviorOperationImplementation
|
|
|
74
74
|
if (inlineObject) {
|
|
75
75
|
const {_key, _type, ...rest} = inlineObject
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
77
|
+
const [focusSpan] = getFocusSpan({editor: operation.editor})
|
|
78
|
+
|
|
79
|
+
if (focusSpan) {
|
|
80
|
+
Transforms.insertNodes(
|
|
81
|
+
operation.editor,
|
|
82
|
+
{
|
|
83
|
+
_key,
|
|
84
|
+
_type,
|
|
85
|
+
children: [
|
|
86
|
+
{
|
|
87
|
+
_key: VOID_CHILD_KEY,
|
|
88
|
+
_type: 'span',
|
|
89
|
+
text: '',
|
|
90
|
+
marks: [],
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
value: rest,
|
|
94
|
+
__inline: true,
|
|
95
|
+
},
|
|
96
|
+
{
|
|
97
|
+
at: focus,
|
|
98
|
+
select: true,
|
|
99
|
+
},
|
|
100
|
+
)
|
|
101
|
+
} else {
|
|
102
|
+
Transforms.insertNodes(
|
|
103
|
+
operation.editor,
|
|
104
|
+
{
|
|
105
|
+
_key,
|
|
106
|
+
_type,
|
|
107
|
+
children: [
|
|
108
|
+
{
|
|
109
|
+
_key: VOID_CHILD_KEY,
|
|
110
|
+
_type: 'span',
|
|
111
|
+
text: '',
|
|
112
|
+
marks: [],
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
value: rest,
|
|
116
|
+
__inline: true,
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
at: [focusBlockIndex, focusChildIndex + 1],
|
|
120
|
+
select: true,
|
|
121
|
+
},
|
|
122
|
+
)
|
|
123
|
+
}
|
|
98
124
|
|
|
99
125
|
return
|
|
100
126
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
getTextSelection,
|
|
14
14
|
} from '../../internal-utils/text-selection'
|
|
15
15
|
import {getValueAnnotations} from '../../internal-utils/value-annotations'
|
|
16
|
-
import {createTestEditor} from '../../test/vitest'
|
|
16
|
+
import {createTestEditor, createTestEditors} from '../../test/vitest'
|
|
17
17
|
import {
|
|
18
18
|
parseBlocks,
|
|
19
19
|
parseInlineObject,
|
|
@@ -25,33 +25,45 @@ import {selectionPointToBlockOffset} from '../../utils/util.selection-point-to-b
|
|
|
25
25
|
import type {Parameter} from '../gherkin-parameter-types'
|
|
26
26
|
import type {Context} from './step-context'
|
|
27
27
|
|
|
28
|
+
const schemaDefinition = defineSchema({
|
|
29
|
+
annotations: [{name: 'comment'}, {name: 'link'}],
|
|
30
|
+
decorators: [{name: 'em'}, {name: 'strong'}],
|
|
31
|
+
blockObjects: [{name: 'image'}, {name: 'break'}],
|
|
32
|
+
inlineObjects: [{name: 'stock-ticker'}],
|
|
33
|
+
lists: [{name: 'bullet'}, {name: 'number'}],
|
|
34
|
+
styles: [
|
|
35
|
+
{name: 'normal'},
|
|
36
|
+
{name: 'h1'},
|
|
37
|
+
{name: 'h2'},
|
|
38
|
+
{name: 'h3'},
|
|
39
|
+
{name: 'h4'},
|
|
40
|
+
{name: 'h5'},
|
|
41
|
+
{name: 'h6'},
|
|
42
|
+
{name: 'blockquote'},
|
|
43
|
+
],
|
|
44
|
+
})
|
|
45
|
+
|
|
28
46
|
/**
|
|
29
47
|
* @internal
|
|
30
48
|
*/
|
|
31
49
|
export const stepDefinitions = [
|
|
32
50
|
Given('one editor', async (context: Context) => {
|
|
33
51
|
const {editor, locator} = await createTestEditor({
|
|
34
|
-
schemaDefinition
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
{name: 'h2'},
|
|
44
|
-
{name: 'h3'},
|
|
45
|
-
{name: 'h4'},
|
|
46
|
-
{name: 'h5'},
|
|
47
|
-
{name: 'h6'},
|
|
48
|
-
{name: 'blockquote'},
|
|
49
|
-
],
|
|
50
|
-
}),
|
|
52
|
+
schemaDefinition,
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
context.locator = locator
|
|
56
|
+
context.editor = editor
|
|
57
|
+
}),
|
|
58
|
+
Given('two editors', async (context: Context) => {
|
|
59
|
+
const {editor, locator, editorB, locatorB} = await createTestEditors({
|
|
60
|
+
schemaDefinition,
|
|
51
61
|
})
|
|
52
62
|
|
|
53
63
|
context.locator = locator
|
|
54
64
|
context.editor = editor
|
|
65
|
+
context.locatorB = locatorB
|
|
66
|
+
context.editorB = editorB
|
|
55
67
|
}),
|
|
56
68
|
|
|
57
69
|
Given('a global keymap', (context: Context) => {
|
|
@@ -62,6 +74,10 @@ export const stepDefinitions = [
|
|
|
62
74
|
await userEvent.click(context.locator)
|
|
63
75
|
}),
|
|
64
76
|
|
|
77
|
+
When('Editor B is focused', async (context: Context) => {
|
|
78
|
+
await userEvent.click(context.locatorB)
|
|
79
|
+
}),
|
|
80
|
+
|
|
65
81
|
Given(
|
|
66
82
|
'the text {terse-pt}',
|
|
67
83
|
(context: Context, tersePt: Parameter['tersePt']) => {
|
|
@@ -172,6 +188,12 @@ export const stepDefinitions = [
|
|
|
172
188
|
When('{string} is typed', async (context: Context, text: string) => {
|
|
173
189
|
await userEvent.type(context.locator, text)
|
|
174
190
|
}),
|
|
191
|
+
When(
|
|
192
|
+
'{string} is typed by Editor B',
|
|
193
|
+
async (context: Context, text: string) => {
|
|
194
|
+
await userEvent.type(context.locatorB, text)
|
|
195
|
+
},
|
|
196
|
+
),
|
|
175
197
|
When('{string} is inserted', (context: Context, text: string) => {
|
|
176
198
|
context.editor.send({type: 'insert.text', text})
|
|
177
199
|
}),
|
|
@@ -276,6 +298,28 @@ export const stepDefinitions = [
|
|
|
276
298
|
})
|
|
277
299
|
},
|
|
278
300
|
),
|
|
301
|
+
When(
|
|
302
|
+
'the caret is put after {string} by Editor B',
|
|
303
|
+
async (context: Context, text: string) => {
|
|
304
|
+
await vi.waitFor(() => {
|
|
305
|
+
const selection = getSelectionAfterText(
|
|
306
|
+
context.editorB.getSnapshot().context,
|
|
307
|
+
text,
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
expect(selection).not.toBeNull()
|
|
311
|
+
|
|
312
|
+
context.editorB.send({
|
|
313
|
+
type: 'select',
|
|
314
|
+
at: selection,
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
expect(context.editorB.getSnapshot().context.selection).toEqual(
|
|
318
|
+
selection,
|
|
319
|
+
)
|
|
320
|
+
})
|
|
321
|
+
},
|
|
322
|
+
),
|
|
279
323
|
Then(
|
|
280
324
|
'the caret is after {string}',
|
|
281
325
|
async (context: Context, text: string) => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type PortableTextEditableProps,
|
|
12
12
|
} from '../../editor/Editable'
|
|
13
13
|
import {EditorProvider} from '../../editor/editor-provider'
|
|
14
|
+
import {EventListenerPlugin} from '../../plugins'
|
|
14
15
|
import {EditorRefPlugin} from '../../plugins/plugin.editor-ref'
|
|
15
16
|
import type {Context} from './step-context'
|
|
16
17
|
|
|
@@ -89,3 +90,96 @@ export async function createTestEditor(
|
|
|
89
90
|
rerender,
|
|
90
91
|
}
|
|
91
92
|
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* @internal
|
|
96
|
+
*/
|
|
97
|
+
export async function createTestEditors(
|
|
98
|
+
options: CreateTestEditorOptions,
|
|
99
|
+
): Promise<Pick<Context, 'editor' | 'locator' | 'editorB' | 'locatorB'>> {
|
|
100
|
+
const editorRef = React.createRef<Editor>()
|
|
101
|
+
const editorBRef = React.createRef<Editor>()
|
|
102
|
+
const keyGenerator = options.keyGenerator ?? createTestKeyGenerator()
|
|
103
|
+
|
|
104
|
+
render(
|
|
105
|
+
<>
|
|
106
|
+
<EditorProvider
|
|
107
|
+
initialConfig={{
|
|
108
|
+
keyGenerator: keyGenerator,
|
|
109
|
+
schemaDefinition: options.schemaDefinition ?? defineSchema({}),
|
|
110
|
+
initialValue: options.initialValue,
|
|
111
|
+
}}
|
|
112
|
+
>
|
|
113
|
+
<EditorRefPlugin ref={editorRef} />
|
|
114
|
+
<PortableTextEditable
|
|
115
|
+
{...options.editableProps}
|
|
116
|
+
data-testid="editor-a"
|
|
117
|
+
/>
|
|
118
|
+
<EventListenerPlugin
|
|
119
|
+
on={(event) => {
|
|
120
|
+
if (event.type === 'mutation') {
|
|
121
|
+
editorBRef.current?.send({
|
|
122
|
+
type: 'patches',
|
|
123
|
+
patches: event.patches.map((patch) => ({
|
|
124
|
+
...patch,
|
|
125
|
+
origin: 'remote',
|
|
126
|
+
})),
|
|
127
|
+
snapshot: event.snapshot,
|
|
128
|
+
})
|
|
129
|
+
editorBRef.current?.send({
|
|
130
|
+
type: 'update value',
|
|
131
|
+
value: event.value,
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
}}
|
|
135
|
+
/>
|
|
136
|
+
{options.children}
|
|
137
|
+
</EditorProvider>
|
|
138
|
+
<EditorProvider
|
|
139
|
+
initialConfig={{
|
|
140
|
+
keyGenerator: keyGenerator,
|
|
141
|
+
schemaDefinition: options.schemaDefinition ?? defineSchema({}),
|
|
142
|
+
initialValue: options.initialValue,
|
|
143
|
+
}}
|
|
144
|
+
>
|
|
145
|
+
<EditorRefPlugin ref={editorBRef} />
|
|
146
|
+
<PortableTextEditable
|
|
147
|
+
{...options.editableProps}
|
|
148
|
+
data-testid="editor-b"
|
|
149
|
+
/>
|
|
150
|
+
<EventListenerPlugin
|
|
151
|
+
on={(event) => {
|
|
152
|
+
if (event.type === 'mutation') {
|
|
153
|
+
editorRef.current?.send({
|
|
154
|
+
type: 'patches',
|
|
155
|
+
patches: event.patches.map((patch) => ({
|
|
156
|
+
...patch,
|
|
157
|
+
origin: 'remote',
|
|
158
|
+
})),
|
|
159
|
+
snapshot: event.value,
|
|
160
|
+
})
|
|
161
|
+
editorRef.current?.send({
|
|
162
|
+
type: 'update value',
|
|
163
|
+
value: event.value,
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
}}
|
|
167
|
+
/>
|
|
168
|
+
{options.children}
|
|
169
|
+
</EditorProvider>
|
|
170
|
+
</>,
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
const locator = page.getByTestId('editor-a')
|
|
174
|
+
const locatorB = page.getByTestId('editor-b')
|
|
175
|
+
|
|
176
|
+
await vi.waitFor(() => expect.element(locator).toBeInTheDocument())
|
|
177
|
+
await vi.waitFor(() => expect.element(locatorB).toBeInTheDocument())
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
editor: editorRef.current!,
|
|
181
|
+
locator,
|
|
182
|
+
editorB: editorBRef.current!,
|
|
183
|
+
locatorB,
|
|
184
|
+
}
|
|
185
|
+
}
|