@portabletext/editor 1.50.3 → 1.50.4
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/behaviors/index.d.cts +1 -0
- package/lib/behaviors/index.d.ts +1 -0
- package/lib/index.cjs +322 -55
- package/lib/index.cjs.map +1 -1
- package/lib/index.d.cts +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +323 -55
- package/lib/index.js.map +1 -1
- package/lib/plugins/index.d.cts +1 -0
- package/lib/plugins/index.d.ts +1 -0
- package/lib/selectors/index.d.cts +1 -0
- package/lib/selectors/index.d.ts +1 -0
- package/lib/utils/index.d.cts +1 -0
- package/lib/utils/index.d.ts +1 -0
- package/package.json +2 -1
- package/src/editor/PortableTextEditor.tsx +22 -22
- package/src/editor/create-slate-editor.tsx +9 -1
- package/src/editor/editor-selector.ts +1 -5
- package/src/editor/editor-snapshot.ts +1 -3
- package/src/editor/plugins/slate-plugin.update-value.ts +30 -0
- package/src/editor/plugins/with-plugins.ts +8 -1
- package/src/internal-utils/apply-operation-to-portable-text.test.ts +175 -0
- package/src/internal-utils/apply-operation-to-portable-text.ts +435 -0
- package/src/internal-utils/create-placeholder-block.ts +20 -0
- package/src/internal-utils/portable-text-node.ts +209 -0
- package/src/types/editor.ts +1 -0
- package/src/internal-utils/__tests__/patchToOperations.test.ts +0 -312
- package/src/internal-utils/slate-children-to-blocks.ts +0 -49
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import type {PortableTextBlock} from '@sanity/types'
|
|
2
|
+
import {createDraft, finishDraft, type WritableDraft} from 'immer'
|
|
3
|
+
import {Element, Path, type Node, type Operation} from 'slate'
|
|
4
|
+
import type {EditorSchema} from '../editor/editor-schema'
|
|
5
|
+
import type {EditorContext} from '../editor/editor-snapshot'
|
|
6
|
+
import type {OmitFromUnion} from '../type-utils'
|
|
7
|
+
import {
|
|
8
|
+
getBlock,
|
|
9
|
+
getNode,
|
|
10
|
+
getParent,
|
|
11
|
+
getSpan,
|
|
12
|
+
isEditorNode,
|
|
13
|
+
isObjectNode,
|
|
14
|
+
isPartialSpanNode,
|
|
15
|
+
isSpanNode,
|
|
16
|
+
isTextBlockNode,
|
|
17
|
+
type PortableTextNode,
|
|
18
|
+
type SpanNode,
|
|
19
|
+
type TextBlockNode,
|
|
20
|
+
} from './portable-text-node'
|
|
21
|
+
|
|
22
|
+
export function applyOperationToPortableText(
|
|
23
|
+
context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
|
|
24
|
+
value: Array<PortableTextBlock>,
|
|
25
|
+
operation: OmitFromUnion<Operation, 'type', 'set_selection'>,
|
|
26
|
+
) {
|
|
27
|
+
const draft = createDraft({children: value})
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
applyOperationToPortableTextDraft(context, draft, operation)
|
|
31
|
+
} catch (e) {
|
|
32
|
+
console.error(e)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return finishDraft(draft).children
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function applyOperationToPortableTextDraft(
|
|
39
|
+
context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
|
|
40
|
+
root: WritableDraft<{
|
|
41
|
+
children: Array<PortableTextBlock>
|
|
42
|
+
}>,
|
|
43
|
+
operation: OmitFromUnion<Operation, 'type', 'set_selection'>,
|
|
44
|
+
) {
|
|
45
|
+
switch (operation.type) {
|
|
46
|
+
case 'insert_node': {
|
|
47
|
+
const {path, node: insertedNode} = operation
|
|
48
|
+
const parent = getParent(context, root, path)
|
|
49
|
+
const index = path[path.length - 1]
|
|
50
|
+
|
|
51
|
+
if (!parent) {
|
|
52
|
+
break
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (index > parent.children.length) {
|
|
56
|
+
break
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (path.length === 1) {
|
|
60
|
+
// Inserting block at the root
|
|
61
|
+
|
|
62
|
+
if (isTextBlockNode(context, insertedNode)) {
|
|
63
|
+
// Text blocks can be inserted as is
|
|
64
|
+
|
|
65
|
+
parent.children.splice(index, 0, {
|
|
66
|
+
...insertedNode,
|
|
67
|
+
children: insertedNode.children.map((child) => {
|
|
68
|
+
if ('__inline' in child) {
|
|
69
|
+
// Except for inline object children which need to have their
|
|
70
|
+
// `value` spread onto the block
|
|
71
|
+
return {
|
|
72
|
+
_key: child._key,
|
|
73
|
+
_type: child._type,
|
|
74
|
+
...('value' in child && typeof child.value === 'object'
|
|
75
|
+
? child.value
|
|
76
|
+
: {}),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return child
|
|
81
|
+
}),
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
break
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (Element.isElement(insertedNode) && !('__inline' in insertedNode)) {
|
|
88
|
+
// Void blocks have to have their `value` spread onto the block
|
|
89
|
+
|
|
90
|
+
parent.children.splice(index, 0, {
|
|
91
|
+
_key: insertedNode._key,
|
|
92
|
+
_type: insertedNode._type,
|
|
93
|
+
...('value' in insertedNode &&
|
|
94
|
+
typeof insertedNode.value === 'object'
|
|
95
|
+
? insertedNode.value
|
|
96
|
+
: {}),
|
|
97
|
+
})
|
|
98
|
+
break
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (path.length === 2) {
|
|
103
|
+
// Inserting children into blocks
|
|
104
|
+
|
|
105
|
+
if (!isTextBlockNode(context, parent)) {
|
|
106
|
+
// Only text blocks can have children
|
|
107
|
+
break
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (isPartialSpanNode(insertedNode)) {
|
|
111
|
+
// Text nodes can be inserted as is
|
|
112
|
+
|
|
113
|
+
parent.children.splice(index, 0, insertedNode)
|
|
114
|
+
break
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if ('__inline' in insertedNode) {
|
|
118
|
+
// Void children have to have their `value` spread onto the block
|
|
119
|
+
|
|
120
|
+
parent.children.splice(index, 0, {
|
|
121
|
+
_key: insertedNode._key,
|
|
122
|
+
_type: insertedNode._type,
|
|
123
|
+
...('value' in insertedNode &&
|
|
124
|
+
typeof insertedNode.value === 'object'
|
|
125
|
+
? insertedNode.value
|
|
126
|
+
: {}),
|
|
127
|
+
})
|
|
128
|
+
break
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
break
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
case 'insert_text': {
|
|
136
|
+
const {path, offset, text} = operation
|
|
137
|
+
if (text.length === 0) break
|
|
138
|
+
const span = getSpan(context, root, path)
|
|
139
|
+
|
|
140
|
+
if (!span) {
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const before = span.text.slice(0, offset)
|
|
145
|
+
const after = span.text.slice(offset)
|
|
146
|
+
span.text = before + text + after
|
|
147
|
+
|
|
148
|
+
break
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
case 'merge_node': {
|
|
152
|
+
const {path} = operation
|
|
153
|
+
const node = getNode(context, root, path)
|
|
154
|
+
const prevPath = Path.previous(path)
|
|
155
|
+
const prev = getNode(context, root, prevPath)
|
|
156
|
+
const parent = getParent(context, root, path)
|
|
157
|
+
|
|
158
|
+
if (!node || !prev || !parent) {
|
|
159
|
+
break
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const index = path[path.length - 1]
|
|
163
|
+
|
|
164
|
+
if (isPartialSpanNode(node) && isPartialSpanNode(prev)) {
|
|
165
|
+
prev.text += node.text
|
|
166
|
+
} else if (
|
|
167
|
+
isTextBlockNode(context, node) &&
|
|
168
|
+
isTextBlockNode(context, prev)
|
|
169
|
+
) {
|
|
170
|
+
prev.children.push(...node.children)
|
|
171
|
+
} else {
|
|
172
|
+
break
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
parent.children.splice(index, 1)
|
|
176
|
+
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
case 'move_node': {
|
|
181
|
+
const {path, newPath} = operation
|
|
182
|
+
|
|
183
|
+
if (Path.isAncestor(path, newPath)) {
|
|
184
|
+
break
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const node = getNode(context, root, path)
|
|
188
|
+
const parent = getParent(context, root, path)
|
|
189
|
+
const index = path[path.length - 1]
|
|
190
|
+
|
|
191
|
+
if (!node || !parent) {
|
|
192
|
+
break
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// This is tricky, but since the `path` and `newPath` both refer to
|
|
196
|
+
// the same snapshot in time, there's a mismatch. After either
|
|
197
|
+
// removing the original position, the second step's path can be out
|
|
198
|
+
// of date. So instead of using the `op.newPath` directly, we
|
|
199
|
+
// transform `op.path` to ascertain what the `newPath` would be after
|
|
200
|
+
// the operation was applied.
|
|
201
|
+
parent.children.splice(index, 1)
|
|
202
|
+
const truePath = Path.transform(path, operation)!
|
|
203
|
+
const newParent = getNode(context, root, Path.parent(truePath))
|
|
204
|
+
const newIndex = truePath[truePath.length - 1]
|
|
205
|
+
|
|
206
|
+
if (!newParent) {
|
|
207
|
+
break
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (!('children' in newParent)) {
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!Array.isArray(newParent.children)) {
|
|
215
|
+
break
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
newParent.children.splice(newIndex, 0, node)
|
|
219
|
+
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
case 'remove_node': {
|
|
224
|
+
const {path} = operation
|
|
225
|
+
const index = path[path.length - 1]
|
|
226
|
+
const parent = getParent(context, root, path)
|
|
227
|
+
parent?.children.splice(index, 1)
|
|
228
|
+
|
|
229
|
+
break
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
case 'remove_text': {
|
|
233
|
+
const {path, offset, text} = operation
|
|
234
|
+
|
|
235
|
+
if (text.length === 0) {
|
|
236
|
+
break
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const span = getSpan(context, root, path)
|
|
240
|
+
|
|
241
|
+
if (!span) {
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const before = span.text.slice(0, offset)
|
|
246
|
+
const after = span.text.slice(offset + text.length)
|
|
247
|
+
span.text = before + after
|
|
248
|
+
|
|
249
|
+
break
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
case 'set_node': {
|
|
253
|
+
const {path, properties, newProperties} = operation
|
|
254
|
+
|
|
255
|
+
const node = getNode(context, root, path)
|
|
256
|
+
|
|
257
|
+
if (!node) {
|
|
258
|
+
break
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (isEditorNode(node)) {
|
|
262
|
+
break
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (isObjectNode(context, node)) {
|
|
266
|
+
const valueBefore = (
|
|
267
|
+
'value' in properties && typeof properties.value === 'object'
|
|
268
|
+
? properties.value
|
|
269
|
+
: {}
|
|
270
|
+
) as Partial<Node>
|
|
271
|
+
const valueAfter = (
|
|
272
|
+
'value' in newProperties && typeof newProperties.value === 'object'
|
|
273
|
+
? newProperties.value
|
|
274
|
+
: {}
|
|
275
|
+
) as Partial<Node>
|
|
276
|
+
|
|
277
|
+
for (const key in newProperties) {
|
|
278
|
+
if (key === 'value') {
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const value = newProperties[key as keyof Partial<Node>]
|
|
283
|
+
|
|
284
|
+
if (value == null) {
|
|
285
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
286
|
+
} else {
|
|
287
|
+
node[<keyof PortableTextNode<EditorSchema>>key] = value
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
for (const key in properties) {
|
|
292
|
+
if (key === 'value') {
|
|
293
|
+
continue
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (!newProperties.hasOwnProperty(key)) {
|
|
297
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
for (const key in valueAfter) {
|
|
302
|
+
const value = valueAfter[key as keyof Partial<Node>]
|
|
303
|
+
|
|
304
|
+
if (value == null) {
|
|
305
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
306
|
+
} else {
|
|
307
|
+
node[<keyof PortableTextNode<EditorSchema>>key] = value
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
for (const key in valueBefore) {
|
|
312
|
+
if (!valueAfter.hasOwnProperty(key)) {
|
|
313
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
break
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (isTextBlockNode(context, node)) {
|
|
321
|
+
for (const key in newProperties) {
|
|
322
|
+
if (key === 'children' || key === 'text') {
|
|
323
|
+
break
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const value = newProperties[key as keyof Partial<Node>]
|
|
327
|
+
|
|
328
|
+
if (value == null) {
|
|
329
|
+
delete node[<keyof Partial<Node>>key]
|
|
330
|
+
} else {
|
|
331
|
+
node[<keyof Partial<Node>>key] = value
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// properties that were previously defined, but are now missing, must be deleted
|
|
336
|
+
for (const key in properties) {
|
|
337
|
+
if (!newProperties.hasOwnProperty(key)) {
|
|
338
|
+
delete node[<keyof Partial<Node>>key]
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (isPartialSpanNode(node)) {
|
|
346
|
+
for (const key in newProperties) {
|
|
347
|
+
if (key === 'text') {
|
|
348
|
+
break
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
const value = newProperties[key as keyof Partial<Node>]
|
|
352
|
+
|
|
353
|
+
if (value == null) {
|
|
354
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
355
|
+
} else {
|
|
356
|
+
node[<keyof PortableTextNode<EditorSchema>>key] = value
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// properties that were previously defined, but are now missing, must be deleted
|
|
361
|
+
for (const key in properties) {
|
|
362
|
+
if (!newProperties.hasOwnProperty(key)) {
|
|
363
|
+
delete node[<keyof PortableTextNode<EditorSchema>>key]
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
break
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
break
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
case 'split_node': {
|
|
374
|
+
const {path, position, properties} = operation
|
|
375
|
+
|
|
376
|
+
if (path.length === 0) {
|
|
377
|
+
break
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parent = getParent(context, root, path)
|
|
381
|
+
const index = path[path.length - 1]
|
|
382
|
+
|
|
383
|
+
if (!parent) {
|
|
384
|
+
break
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
if (isEditorNode(parent)) {
|
|
388
|
+
const block = getBlock(root, path)
|
|
389
|
+
|
|
390
|
+
if (!block || !isTextBlockNode(context, block)) {
|
|
391
|
+
break
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const before = block.children.slice(0, position)
|
|
395
|
+
const after = block.children.slice(position)
|
|
396
|
+
block.children = before
|
|
397
|
+
|
|
398
|
+
// _key is deliberately left out
|
|
399
|
+
const newTextBlockNode = {
|
|
400
|
+
...properties,
|
|
401
|
+
children: after,
|
|
402
|
+
_type: context.schema.block.name,
|
|
403
|
+
} as unknown as TextBlockNode<EditorSchema>
|
|
404
|
+
|
|
405
|
+
parent.children.splice(index + 1, 0, newTextBlockNode)
|
|
406
|
+
|
|
407
|
+
break
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (isTextBlockNode(context, parent)) {
|
|
411
|
+
const node = getNode(context, root, path)
|
|
412
|
+
|
|
413
|
+
if (!node || !isSpanNode(context, node)) {
|
|
414
|
+
break
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const before = node.text.slice(0, position)
|
|
418
|
+
const after = node.text.slice(position)
|
|
419
|
+
node.text = before
|
|
420
|
+
|
|
421
|
+
// _key is deliberately left out
|
|
422
|
+
const newSpanNode = {
|
|
423
|
+
...properties,
|
|
424
|
+
text: after,
|
|
425
|
+
} as unknown as SpanNode<EditorSchema>
|
|
426
|
+
|
|
427
|
+
parent.children.splice(index + 1, 0, newSpanNode)
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
break
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
return root
|
|
435
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {EditorContext} from '../editor/editor-snapshot'
|
|
2
|
+
|
|
3
|
+
export function createPlaceholderBlock(
|
|
4
|
+
context: Pick<EditorContext, 'keyGenerator' | 'schema'>,
|
|
5
|
+
) {
|
|
6
|
+
return {
|
|
7
|
+
_type: context.schema.block.name,
|
|
8
|
+
_key: context.keyGenerator(),
|
|
9
|
+
style: context.schema.styles[0].name ?? 'normal',
|
|
10
|
+
markDefs: [],
|
|
11
|
+
children: [
|
|
12
|
+
{
|
|
13
|
+
_type: context.schema.span.name,
|
|
14
|
+
_key: context.keyGenerator(),
|
|
15
|
+
text: '',
|
|
16
|
+
marks: [],
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type {EditorSchema} from '../editor/editor-schema'
|
|
2
|
+
import {isTypedObject} from './asserters'
|
|
3
|
+
|
|
4
|
+
type Path = Array<number>
|
|
5
|
+
|
|
6
|
+
export type PortableTextNode<TEditorSchema extends EditorSchema> =
|
|
7
|
+
| EditorNode<TEditorSchema>
|
|
8
|
+
| TextBlockNode<TEditorSchema>
|
|
9
|
+
| SpanNode<TEditorSchema>
|
|
10
|
+
| PartialSpanNode
|
|
11
|
+
| ObjectNode
|
|
12
|
+
|
|
13
|
+
//////////
|
|
14
|
+
|
|
15
|
+
export type EditorNode<TEditorSchema extends EditorSchema> = {
|
|
16
|
+
children: Array<TextBlockNode<TEditorSchema> | ObjectNode>
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isEditorNode<TEditorSchema extends EditorSchema>(
|
|
20
|
+
node: unknown,
|
|
21
|
+
): node is EditorNode<TEditorSchema> {
|
|
22
|
+
if (typeof node === 'object' && node !== null) {
|
|
23
|
+
return (
|
|
24
|
+
!('_type' in node) && 'children' in node && Array.isArray(node.children)
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return false
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
//////////
|
|
32
|
+
|
|
33
|
+
export type TextBlockNode<TEditorSchema extends EditorSchema> = {
|
|
34
|
+
_key: string
|
|
35
|
+
_type: TEditorSchema['block']['name']
|
|
36
|
+
children: Array<SpanNode<TEditorSchema> | ObjectNode>
|
|
37
|
+
[other: string]: unknown
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function isTextBlockNode<TEditorSchema extends EditorSchema>(
|
|
41
|
+
context: {schema: TEditorSchema},
|
|
42
|
+
node: unknown,
|
|
43
|
+
): node is TextBlockNode<TEditorSchema> {
|
|
44
|
+
return isTypedObject(node) && node._type === context.schema.block.name
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
//////////
|
|
48
|
+
|
|
49
|
+
export type SpanNode<TEditorSchema extends EditorSchema> = {
|
|
50
|
+
_key: string
|
|
51
|
+
_type?: TEditorSchema['span']['name']
|
|
52
|
+
text: string
|
|
53
|
+
[other: string]: unknown
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function isSpanNode<TEditorSchema extends EditorSchema>(
|
|
57
|
+
context: {schema: TEditorSchema},
|
|
58
|
+
node: unknown,
|
|
59
|
+
): node is SpanNode<TEditorSchema> {
|
|
60
|
+
if (typeof node !== 'object' || node === null) {
|
|
61
|
+
return false
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ('children' in node) {
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if ('_type' in node) {
|
|
69
|
+
return node._type === context.schema.span.name
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return 'text' in node
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
//////////
|
|
76
|
+
|
|
77
|
+
export type PartialSpanNode = {
|
|
78
|
+
text: string
|
|
79
|
+
[other: string]: unknown
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function isPartialSpanNode(node: unknown): node is PartialSpanNode {
|
|
83
|
+
return (
|
|
84
|
+
typeof node === 'object' &&
|
|
85
|
+
node !== null &&
|
|
86
|
+
'text' in node &&
|
|
87
|
+
typeof node.text === 'string'
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
//////////
|
|
92
|
+
|
|
93
|
+
export type ObjectNode = {
|
|
94
|
+
_type: string
|
|
95
|
+
_key: string
|
|
96
|
+
[other: string]: unknown
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function isObjectNode(
|
|
100
|
+
context: {schema: EditorSchema},
|
|
101
|
+
node: unknown,
|
|
102
|
+
): node is ObjectNode {
|
|
103
|
+
return (
|
|
104
|
+
!isEditorNode(node) &&
|
|
105
|
+
!isTextBlockNode(context, node) &&
|
|
106
|
+
!isSpanNode(context, node) &&
|
|
107
|
+
!isPartialSpanNode(node)
|
|
108
|
+
)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
*
|
|
113
|
+
*/
|
|
114
|
+
export function getBlock<TEditorSchema extends EditorSchema>(
|
|
115
|
+
root: EditorNode<TEditorSchema>,
|
|
116
|
+
path: Path,
|
|
117
|
+
): TextBlockNode<TEditorSchema> | ObjectNode | undefined {
|
|
118
|
+
const index = path.at(0)
|
|
119
|
+
|
|
120
|
+
if (index === undefined || path.length !== 1) {
|
|
121
|
+
return undefined
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return root.children.at(index)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A "node" can either be
|
|
129
|
+
* 1. The root (path length is 0)
|
|
130
|
+
* 2. A block (path length is 1)
|
|
131
|
+
* 3. A span (path length is 2)
|
|
132
|
+
* 4. Or an inline object (path length is 2)
|
|
133
|
+
*/
|
|
134
|
+
export function getNode<TEditorSchema extends EditorSchema>(
|
|
135
|
+
context: {schema: TEditorSchema},
|
|
136
|
+
root: EditorNode<TEditorSchema>,
|
|
137
|
+
path: Path,
|
|
138
|
+
): PortableTextNode<TEditorSchema> | undefined {
|
|
139
|
+
if (path.length === 0) {
|
|
140
|
+
return root
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (path.length === 1) {
|
|
144
|
+
return getBlock(root, path)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (path.length === 2) {
|
|
148
|
+
const block = getBlock(root, path.slice(0, 1))
|
|
149
|
+
|
|
150
|
+
if (!block || !isTextBlockNode(context, block)) {
|
|
151
|
+
return undefined
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const child = block.children.at(path[1])
|
|
155
|
+
|
|
156
|
+
if (!child) {
|
|
157
|
+
return undefined
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return child
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export function getSpan<TEditorSchema extends EditorSchema>(
|
|
165
|
+
context: {schema: TEditorSchema},
|
|
166
|
+
root: EditorNode<TEditorSchema>,
|
|
167
|
+
path: Path,
|
|
168
|
+
) {
|
|
169
|
+
const node = getNode(context, root, path)
|
|
170
|
+
|
|
171
|
+
if (node && isSpanNode(context, node)) {
|
|
172
|
+
return node
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return undefined
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* A parent can either be the root or a text block
|
|
180
|
+
*/
|
|
181
|
+
export function getParent<TEditorSchema extends EditorSchema>(
|
|
182
|
+
context: {schema: TEditorSchema},
|
|
183
|
+
root: EditorNode<TEditorSchema>,
|
|
184
|
+
path: Path,
|
|
185
|
+
) {
|
|
186
|
+
if (path.length === 0) {
|
|
187
|
+
return undefined
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parentPath = path.slice(0, -1)
|
|
191
|
+
|
|
192
|
+
if (parentPath.length === 0) {
|
|
193
|
+
return root
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const blockIndex = parentPath.at(0)
|
|
197
|
+
|
|
198
|
+
if (blockIndex === undefined || parentPath.length !== 1) {
|
|
199
|
+
return undefined
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const block = root.children.at(blockIndex)
|
|
203
|
+
|
|
204
|
+
if (block && isTextBlockNode(context, block)) {
|
|
205
|
+
return block
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return undefined
|
|
209
|
+
}
|
package/src/types/editor.ts
CHANGED
|
@@ -124,6 +124,7 @@ export interface PortableTextSlateEditor extends ReactEditor {
|
|
|
124
124
|
isTextBlock: (value: unknown) => value is PortableTextTextBlock
|
|
125
125
|
isTextSpan: (value: unknown) => value is PortableTextSpan
|
|
126
126
|
isListBlock: (value: unknown) => value is PortableTextListBlock
|
|
127
|
+
value: Array<PortableTextBlock>
|
|
127
128
|
|
|
128
129
|
/**
|
|
129
130
|
* Use hotkeys
|