@portabletext/editor 1.11.3 → 1.12.1
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/README.md +11 -0
- package/lib/index.d.mts +26 -7
- package/lib/index.d.ts +26 -7
- package/lib/index.esm.js +427 -152
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +426 -151
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +427 -152
- package/lib/index.mjs.map +1 -1
- package/package.json +12 -12
- package/src/editor/behavior/behavior.action-utils.insert-block.ts +63 -0
- package/src/editor/behavior/behavior.action.insert-block-object.ts +25 -0
- package/src/editor/behavior/behavior.actions.ts +88 -32
- package/src/editor/behavior/behavior.core.block-objects.ts +5 -11
- package/src/editor/behavior/behavior.markdown.ts +162 -69
- package/src/editor/behavior/behavior.types.ts +22 -6
- package/src/editor/behavior/behavior.utils.block-offset.test.ts +143 -0
- package/src/editor/behavior/behavior.utils.block-offset.ts +101 -0
- package/src/editor/behavior/behavior.utils.get-selection-text.ts +92 -0
- package/src/editor/behavior/behavior.utils.get-start-point.ts +26 -0
- package/src/editor/behavior/behavior.utils.is-keyed-segment.ts +5 -0
- package/src/editor/behavior/behavior.utils.reverse-selection.ts +21 -0
- package/src/editor/behavior/behavior.utils.ts +13 -2
- package/src/editor/behavior/behavior.utilts.get-text-before.ts +31 -0
- package/src/editor/plugins/createWithEditableAPI.ts +22 -87
- package/src/index.ts +1 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {isPortableTextTextBlock} from '@sanity/types'
|
|
2
2
|
import type {PortableTextMemberSchemaTypes} from '../../types/editor'
|
|
3
3
|
import {defineBehavior} from './behavior.types'
|
|
4
4
|
import {
|
|
5
|
+
getFocusBlock,
|
|
5
6
|
getFocusSpan,
|
|
6
7
|
getFocusTextBlock,
|
|
8
|
+
getTextBlockText,
|
|
7
9
|
selectionIsCollapsed,
|
|
8
10
|
} from './behavior.utils'
|
|
11
|
+
import {spanSelectionPointToBlockOffset} from './behavior.utils.block-offset'
|
|
12
|
+
import {getBlockTextBefore} from './behavior.utilts.get-text-before'
|
|
9
13
|
|
|
10
14
|
/**
|
|
11
15
|
* @alpha
|
|
@@ -53,8 +57,25 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
53
57
|
return false
|
|
54
58
|
}
|
|
55
59
|
|
|
56
|
-
const
|
|
57
|
-
|
|
60
|
+
const blockOffset = spanSelectionPointToBlockOffset({
|
|
61
|
+
value: context.value,
|
|
62
|
+
selectionPoint: {
|
|
63
|
+
path: [
|
|
64
|
+
{_key: focusTextBlock.node._key},
|
|
65
|
+
'children',
|
|
66
|
+
{_key: focusSpan.node._key},
|
|
67
|
+
],
|
|
68
|
+
offset: context.selection.focus.offset,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
if (!blockOffset) {
|
|
73
|
+
return false
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const blockText = getTextBlockText(focusTextBlock.node)
|
|
77
|
+
const caretAtTheEndOfQuote = blockOffset.offset === 1
|
|
78
|
+
const looksLikeMarkdownQuote = /^>/.test(blockText)
|
|
58
79
|
const blockquoteStyle = config.blockquoteStyle?.({schema: context.schema})
|
|
59
80
|
|
|
60
81
|
if (
|
|
@@ -62,7 +83,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
62
83
|
looksLikeMarkdownQuote &&
|
|
63
84
|
blockquoteStyle !== undefined
|
|
64
85
|
) {
|
|
65
|
-
return {focusTextBlock,
|
|
86
|
+
return {focusTextBlock, style: blockquoteStyle}
|
|
66
87
|
}
|
|
67
88
|
|
|
68
89
|
return false
|
|
@@ -74,7 +95,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
74
95
|
text: ' ',
|
|
75
96
|
},
|
|
76
97
|
],
|
|
77
|
-
(_, {focusTextBlock,
|
|
98
|
+
(_, {focusTextBlock, style}) => [
|
|
78
99
|
{
|
|
79
100
|
type: 'unset block',
|
|
80
101
|
props: ['listItem', 'level'],
|
|
@@ -86,22 +107,20 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
86
107
|
paths: [focusTextBlock.path],
|
|
87
108
|
},
|
|
88
109
|
{
|
|
89
|
-
type: 'delete',
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
offset: 2,
|
|
98
|
-
},
|
|
110
|
+
type: 'delete text',
|
|
111
|
+
anchor: {
|
|
112
|
+
path: focusTextBlock.path,
|
|
113
|
+
offset: 0,
|
|
114
|
+
},
|
|
115
|
+
focus: {
|
|
116
|
+
path: focusTextBlock.path,
|
|
117
|
+
offset: 2,
|
|
99
118
|
},
|
|
100
119
|
},
|
|
101
120
|
],
|
|
102
121
|
],
|
|
103
122
|
})
|
|
104
|
-
const
|
|
123
|
+
const automaticHr = defineBehavior({
|
|
105
124
|
on: 'insert text',
|
|
106
125
|
guard: ({context, event}) => {
|
|
107
126
|
const hrCharacter =
|
|
@@ -117,23 +136,33 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
117
136
|
return false
|
|
118
137
|
}
|
|
119
138
|
|
|
120
|
-
const
|
|
139
|
+
const hrObject = config.horizontalRuleObject?.({
|
|
121
140
|
schema: context.schema,
|
|
122
141
|
})
|
|
123
142
|
const focusBlock = getFocusTextBlock(context)
|
|
124
143
|
const selectionCollapsed = selectionIsCollapsed(context)
|
|
125
144
|
|
|
126
|
-
if (!
|
|
145
|
+
if (!hrObject || !focusBlock || !selectionCollapsed) {
|
|
127
146
|
return false
|
|
128
147
|
}
|
|
129
148
|
|
|
130
|
-
const
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
149
|
+
const textBefore = getBlockTextBefore({
|
|
150
|
+
value: context.value,
|
|
151
|
+
point: context.selection.focus,
|
|
152
|
+
})
|
|
153
|
+
const hrBlockOffsets = {
|
|
154
|
+
anchor: {
|
|
155
|
+
path: focusBlock.path,
|
|
156
|
+
offset: 0,
|
|
157
|
+
},
|
|
158
|
+
focus: {
|
|
159
|
+
path: focusBlock.path,
|
|
160
|
+
offset: 3,
|
|
161
|
+
},
|
|
162
|
+
}
|
|
134
163
|
|
|
135
|
-
if (
|
|
136
|
-
return {
|
|
164
|
+
if (textBefore === `${hrCharacter}${hrCharacter}`) {
|
|
165
|
+
return {hrObject, focusBlock, hrCharacter, hrBlockOffsets}
|
|
137
166
|
}
|
|
138
167
|
|
|
139
168
|
return false
|
|
@@ -145,29 +174,65 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
145
174
|
text: hrCharacter,
|
|
146
175
|
},
|
|
147
176
|
],
|
|
148
|
-
(_, {
|
|
177
|
+
(_, {hrObject, hrBlockOffsets}) => [
|
|
149
178
|
{
|
|
150
179
|
type: 'insert block object',
|
|
151
|
-
|
|
180
|
+
placement: 'before',
|
|
181
|
+
blockObject: hrObject,
|
|
152
182
|
},
|
|
153
183
|
{
|
|
154
|
-
type: 'delete',
|
|
155
|
-
|
|
156
|
-
anchor: {
|
|
157
|
-
path: focusBlock.path,
|
|
158
|
-
offset: 0,
|
|
159
|
-
},
|
|
160
|
-
focus: {
|
|
161
|
-
path: focusBlock.path,
|
|
162
|
-
offset: 0,
|
|
163
|
-
},
|
|
164
|
-
},
|
|
184
|
+
type: 'delete text',
|
|
185
|
+
...hrBlockOffsets,
|
|
165
186
|
},
|
|
187
|
+
],
|
|
188
|
+
],
|
|
189
|
+
})
|
|
190
|
+
const automaticHrOnPaste = defineBehavior({
|
|
191
|
+
on: 'paste',
|
|
192
|
+
guard: ({context, event}) => {
|
|
193
|
+
const text = event.clipboardData.getData('text/plain')
|
|
194
|
+
const hrRegExp = /^(---)$|(___)$|(\*\*\*)$/gm
|
|
195
|
+
const hrCharacters = text.match(hrRegExp)?.[0]
|
|
196
|
+
const hrObject = config.horizontalRuleObject?.({
|
|
197
|
+
schema: context.schema,
|
|
198
|
+
})
|
|
199
|
+
const focusBlock = getFocusBlock(context)
|
|
200
|
+
|
|
201
|
+
if (!hrCharacters || !hrObject || !focusBlock) {
|
|
202
|
+
return false
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return {hrCharacters, hrObject, focusBlock}
|
|
206
|
+
},
|
|
207
|
+
actions: [
|
|
208
|
+
(_, {hrCharacters}) => [
|
|
166
209
|
{
|
|
167
|
-
type: 'insert text
|
|
168
|
-
|
|
210
|
+
type: 'insert text',
|
|
211
|
+
text: hrCharacters,
|
|
169
212
|
},
|
|
170
213
|
],
|
|
214
|
+
(_, {hrObject, focusBlock}) =>
|
|
215
|
+
isPortableTextTextBlock(focusBlock.node)
|
|
216
|
+
? [
|
|
217
|
+
{
|
|
218
|
+
type: 'insert text block',
|
|
219
|
+
textBlock: {children: focusBlock.node.children},
|
|
220
|
+
placement: 'after',
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
type: 'insert block object',
|
|
224
|
+
blockObject: hrObject,
|
|
225
|
+
placement: 'after',
|
|
226
|
+
},
|
|
227
|
+
{type: 'delete block', blockPath: focusBlock.path},
|
|
228
|
+
]
|
|
229
|
+
: [
|
|
230
|
+
{
|
|
231
|
+
type: 'insert block object',
|
|
232
|
+
blockObject: hrObject,
|
|
233
|
+
placement: 'after',
|
|
234
|
+
},
|
|
235
|
+
],
|
|
171
236
|
],
|
|
172
237
|
})
|
|
173
238
|
const automaticHeadingOnSpace = defineBehavior({
|
|
@@ -187,11 +252,28 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
187
252
|
return false
|
|
188
253
|
}
|
|
189
254
|
|
|
190
|
-
const
|
|
255
|
+
const blockOffset = spanSelectionPointToBlockOffset({
|
|
256
|
+
value: context.value,
|
|
257
|
+
selectionPoint: {
|
|
258
|
+
path: [
|
|
259
|
+
{_key: focusTextBlock.node._key},
|
|
260
|
+
'children',
|
|
261
|
+
{_key: focusSpan.node._key},
|
|
262
|
+
],
|
|
263
|
+
offset: context.selection.focus.offset,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
if (!blockOffset) {
|
|
268
|
+
return false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const blockText = getTextBlockText(focusTextBlock.node)
|
|
272
|
+
const markdownHeadingSearch = /^#+/.exec(blockText)
|
|
191
273
|
const level = markdownHeadingSearch
|
|
192
274
|
? markdownHeadingSearch[0].length
|
|
193
275
|
: undefined
|
|
194
|
-
const caretAtTheEndOfHeading =
|
|
276
|
+
const caretAtTheEndOfHeading = blockOffset.offset === level
|
|
195
277
|
|
|
196
278
|
if (!caretAtTheEndOfHeading) {
|
|
197
279
|
return false
|
|
@@ -205,7 +287,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
205
287
|
if (level !== undefined && style !== undefined) {
|
|
206
288
|
return {
|
|
207
289
|
focusTextBlock,
|
|
208
|
-
focusSpan,
|
|
209
290
|
style: style,
|
|
210
291
|
level,
|
|
211
292
|
}
|
|
@@ -220,7 +301,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
220
301
|
text: ' ',
|
|
221
302
|
},
|
|
222
303
|
],
|
|
223
|
-
(_, {focusTextBlock,
|
|
304
|
+
(_, {focusTextBlock, style, level}) => [
|
|
224
305
|
{
|
|
225
306
|
type: 'unset block',
|
|
226
307
|
props: ['listItem', 'level'],
|
|
@@ -232,16 +313,14 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
232
313
|
paths: [focusTextBlock.path],
|
|
233
314
|
},
|
|
234
315
|
{
|
|
235
|
-
type: 'delete',
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
offset: level + 1,
|
|
244
|
-
},
|
|
316
|
+
type: 'delete text',
|
|
317
|
+
anchor: {
|
|
318
|
+
path: focusTextBlock.path,
|
|
319
|
+
offset: 0,
|
|
320
|
+
},
|
|
321
|
+
focus: {
|
|
322
|
+
path: focusTextBlock.path,
|
|
323
|
+
offset: level + 1,
|
|
245
324
|
},
|
|
246
325
|
},
|
|
247
326
|
],
|
|
@@ -301,12 +380,29 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
301
380
|
return false
|
|
302
381
|
}
|
|
303
382
|
|
|
383
|
+
const blockOffset = spanSelectionPointToBlockOffset({
|
|
384
|
+
value: context.value,
|
|
385
|
+
selectionPoint: {
|
|
386
|
+
path: [
|
|
387
|
+
{_key: focusTextBlock.node._key},
|
|
388
|
+
'children',
|
|
389
|
+
{_key: focusSpan.node._key},
|
|
390
|
+
],
|
|
391
|
+
offset: context.selection.focus.offset,
|
|
392
|
+
},
|
|
393
|
+
})
|
|
394
|
+
|
|
395
|
+
if (!blockOffset) {
|
|
396
|
+
return false
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const blockText = getTextBlockText(focusTextBlock.node)
|
|
304
400
|
const defaultStyle = config.defaultStyle?.({schema: context.schema})
|
|
305
|
-
const looksLikeUnorderedList = /^(-|\*)/.test(
|
|
401
|
+
const looksLikeUnorderedList = /^(-|\*)/.test(blockText)
|
|
306
402
|
const unorderedListStyle = config.unorderedListStyle?.({
|
|
307
403
|
schema: context.schema,
|
|
308
404
|
})
|
|
309
|
-
const caretAtTheEndOfUnorderedList =
|
|
405
|
+
const caretAtTheEndOfUnorderedList = blockOffset.offset === 1
|
|
310
406
|
|
|
311
407
|
if (
|
|
312
408
|
defaultStyle &&
|
|
@@ -316,7 +412,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
316
412
|
) {
|
|
317
413
|
return {
|
|
318
414
|
focusTextBlock,
|
|
319
|
-
focusSpan,
|
|
320
415
|
listItem: unorderedListStyle,
|
|
321
416
|
listItemLength: 1,
|
|
322
417
|
style: defaultStyle,
|
|
@@ -337,7 +432,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
337
432
|
) {
|
|
338
433
|
return {
|
|
339
434
|
focusTextBlock,
|
|
340
|
-
focusSpan,
|
|
341
435
|
listItem: orderedListStyle,
|
|
342
436
|
listItemLength: 2,
|
|
343
437
|
style: defaultStyle,
|
|
@@ -353,7 +447,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
353
447
|
text: ' ',
|
|
354
448
|
},
|
|
355
449
|
],
|
|
356
|
-
(_, {focusTextBlock,
|
|
450
|
+
(_, {focusTextBlock, style, listItem, listItemLength}) => [
|
|
357
451
|
{
|
|
358
452
|
type: 'set block',
|
|
359
453
|
listItem,
|
|
@@ -362,16 +456,14 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
362
456
|
paths: [focusTextBlock.path],
|
|
363
457
|
},
|
|
364
458
|
{
|
|
365
|
-
type: 'delete',
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
offset: listItemLength + 1,
|
|
374
|
-
},
|
|
459
|
+
type: 'delete text',
|
|
460
|
+
anchor: {
|
|
461
|
+
path: focusTextBlock.path,
|
|
462
|
+
offset: 0,
|
|
463
|
+
},
|
|
464
|
+
focus: {
|
|
465
|
+
path: focusTextBlock.path,
|
|
466
|
+
offset: listItemLength + 1,
|
|
375
467
|
},
|
|
376
468
|
},
|
|
377
469
|
],
|
|
@@ -380,8 +472,9 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
380
472
|
|
|
381
473
|
const markdownBehaviors = [
|
|
382
474
|
automaticBlockquoteOnSpace,
|
|
383
|
-
automaticBreak,
|
|
384
475
|
automaticHeadingOnSpace,
|
|
476
|
+
automaticHr,
|
|
477
|
+
automaticHrOnPaste,
|
|
385
478
|
clearStyleOnBackspace,
|
|
386
479
|
automaticListOnSpace,
|
|
387
480
|
]
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
KeyedSegment,
|
|
3
|
+
PortableTextBlock,
|
|
4
|
+
PortableTextTextBlock,
|
|
5
|
+
} from '@sanity/types'
|
|
2
6
|
import type {TextUnit} from 'slate'
|
|
3
7
|
import type {TextInsertTextOptions} from 'slate/dist/interfaces/transforms/text'
|
|
4
8
|
import type {
|
|
@@ -6,6 +10,7 @@ import type {
|
|
|
6
10
|
PortableTextMemberSchemaTypes,
|
|
7
11
|
PortableTextSlateEditor,
|
|
8
12
|
} from '../../types/editor'
|
|
13
|
+
import type {BlockOffset} from './behavior.utils.block-offset'
|
|
9
14
|
|
|
10
15
|
/**
|
|
11
16
|
* @alpha
|
|
@@ -100,8 +105,11 @@ export type BehaviorActionIntend =
|
|
|
100
105
|
| BehaviorEvent
|
|
101
106
|
| {
|
|
102
107
|
type: 'insert block object'
|
|
103
|
-
|
|
104
|
-
|
|
108
|
+
placement: 'auto' | 'after' | 'before'
|
|
109
|
+
blockObject: {
|
|
110
|
+
name: string
|
|
111
|
+
value?: {[prop: string]: unknown}
|
|
112
|
+
}
|
|
105
113
|
}
|
|
106
114
|
| {
|
|
107
115
|
type: 'insert span'
|
|
@@ -114,7 +122,10 @@ export type BehaviorActionIntend =
|
|
|
114
122
|
}
|
|
115
123
|
| {
|
|
116
124
|
type: 'insert text block'
|
|
117
|
-
|
|
125
|
+
placement: 'auto' | 'after' | 'before'
|
|
126
|
+
textBlock?: {
|
|
127
|
+
children?: PortableTextTextBlock['children']
|
|
128
|
+
}
|
|
118
129
|
}
|
|
119
130
|
| {
|
|
120
131
|
type: 'set block'
|
|
@@ -129,8 +140,13 @@ export type BehaviorActionIntend =
|
|
|
129
140
|
props: Array<'style' | 'listItem' | 'level'>
|
|
130
141
|
}
|
|
131
142
|
| {
|
|
132
|
-
type: 'delete'
|
|
133
|
-
|
|
143
|
+
type: 'delete block'
|
|
144
|
+
blockPath: [KeyedSegment]
|
|
145
|
+
}
|
|
146
|
+
| {
|
|
147
|
+
type: 'delete text'
|
|
148
|
+
anchor: BlockOffset
|
|
149
|
+
focus: BlockOffset
|
|
134
150
|
}
|
|
135
151
|
| {
|
|
136
152
|
type: 'effect'
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type {PortableTextBlock} from '@sanity/types'
|
|
2
|
+
import {expect, test} from 'vitest'
|
|
3
|
+
import {blockOffsetToSpanSelectionPoint} from './behavior.utils.block-offset'
|
|
4
|
+
|
|
5
|
+
test(blockOffsetToSpanSelectionPoint.name, () => {
|
|
6
|
+
const value: Array<PortableTextBlock> = [
|
|
7
|
+
{
|
|
8
|
+
_key: 'b1',
|
|
9
|
+
_type: 'image',
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
_key: 'b2',
|
|
13
|
+
_type: 'block',
|
|
14
|
+
children: [
|
|
15
|
+
{
|
|
16
|
+
_key: 's1',
|
|
17
|
+
_type: 'span',
|
|
18
|
+
text: 'Hello, ',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
_key: 's2',
|
|
22
|
+
_type: 'span',
|
|
23
|
+
text: 'world!',
|
|
24
|
+
marks: ['strong'],
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
_key: 'b3',
|
|
30
|
+
_type: 'block',
|
|
31
|
+
children: [
|
|
32
|
+
{
|
|
33
|
+
_key: 's3',
|
|
34
|
+
_type: 'span',
|
|
35
|
+
text: 'Here is a ',
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
_key: 's4',
|
|
39
|
+
_type: 'stock-ticker',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
_key: 's5',
|
|
43
|
+
_type: 'span',
|
|
44
|
+
text: '.',
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
_key: 'b4',
|
|
50
|
+
_type: 'block',
|
|
51
|
+
children: [
|
|
52
|
+
{
|
|
53
|
+
_key: 's6',
|
|
54
|
+
_type: 'stock-ticker',
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
_key: 's7',
|
|
58
|
+
_type: 'stock-ticker',
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
_key: 's8',
|
|
62
|
+
_type: 'stock-ticker',
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
},
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
expect(
|
|
69
|
+
blockOffsetToSpanSelectionPoint({
|
|
70
|
+
value,
|
|
71
|
+
blockOffset: {
|
|
72
|
+
path: [{_key: 'b1'}],
|
|
73
|
+
offset: 0,
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
).toBeUndefined()
|
|
77
|
+
expect(
|
|
78
|
+
blockOffsetToSpanSelectionPoint({
|
|
79
|
+
value,
|
|
80
|
+
blockOffset: {
|
|
81
|
+
path: [{_key: 'b2'}],
|
|
82
|
+
offset: 9,
|
|
83
|
+
},
|
|
84
|
+
}),
|
|
85
|
+
).toEqual({
|
|
86
|
+
path: [{_key: 'b2'}, 'children', {_key: 's2'}],
|
|
87
|
+
offset: 2,
|
|
88
|
+
})
|
|
89
|
+
expect(
|
|
90
|
+
blockOffsetToSpanSelectionPoint({
|
|
91
|
+
value,
|
|
92
|
+
blockOffset: {
|
|
93
|
+
path: [{_key: 'b3'}],
|
|
94
|
+
offset: 9,
|
|
95
|
+
},
|
|
96
|
+
}),
|
|
97
|
+
).toEqual({
|
|
98
|
+
path: [{_key: 'b3'}, 'children', {_key: 's3'}],
|
|
99
|
+
offset: 9,
|
|
100
|
+
})
|
|
101
|
+
expect(
|
|
102
|
+
blockOffsetToSpanSelectionPoint({
|
|
103
|
+
value,
|
|
104
|
+
blockOffset: {
|
|
105
|
+
path: [{_key: 'b3'}],
|
|
106
|
+
offset: 10,
|
|
107
|
+
},
|
|
108
|
+
}),
|
|
109
|
+
).toEqual({
|
|
110
|
+
path: [{_key: 'b3'}, 'children', {_key: 's3'}],
|
|
111
|
+
offset: 10,
|
|
112
|
+
})
|
|
113
|
+
expect(
|
|
114
|
+
blockOffsetToSpanSelectionPoint({
|
|
115
|
+
value,
|
|
116
|
+
blockOffset: {
|
|
117
|
+
path: [{_key: 'b3'}],
|
|
118
|
+
offset: 11,
|
|
119
|
+
},
|
|
120
|
+
}),
|
|
121
|
+
).toEqual({
|
|
122
|
+
path: [{_key: 'b3'}, 'children', {_key: 's5'}],
|
|
123
|
+
offset: 1,
|
|
124
|
+
})
|
|
125
|
+
expect(
|
|
126
|
+
blockOffsetToSpanSelectionPoint({
|
|
127
|
+
value,
|
|
128
|
+
blockOffset: {
|
|
129
|
+
path: [{_key: 'b4'}],
|
|
130
|
+
offset: 0,
|
|
131
|
+
},
|
|
132
|
+
}),
|
|
133
|
+
).toBeUndefined()
|
|
134
|
+
expect(
|
|
135
|
+
blockOffsetToSpanSelectionPoint({
|
|
136
|
+
value,
|
|
137
|
+
blockOffset: {
|
|
138
|
+
path: [{_key: 'b4'}],
|
|
139
|
+
offset: 1,
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
).toBeUndefined()
|
|
143
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isPortableTextSpan,
|
|
3
|
+
isPortableTextTextBlock,
|
|
4
|
+
type KeyedSegment,
|
|
5
|
+
type PortableTextBlock,
|
|
6
|
+
} from '@sanity/types'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @alpha
|
|
10
|
+
*/
|
|
11
|
+
export type BlockOffset = {
|
|
12
|
+
path: [KeyedSegment]
|
|
13
|
+
offset: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function blockOffsetToSpanSelectionPoint({
|
|
17
|
+
value,
|
|
18
|
+
blockOffset,
|
|
19
|
+
}: {
|
|
20
|
+
value: Array<PortableTextBlock>
|
|
21
|
+
blockOffset: BlockOffset
|
|
22
|
+
}) {
|
|
23
|
+
let offsetLeft = blockOffset.offset
|
|
24
|
+
let selectionPoint:
|
|
25
|
+
| {path: [KeyedSegment, 'children', KeyedSegment]; offset: number}
|
|
26
|
+
| undefined
|
|
27
|
+
|
|
28
|
+
for (const block of value) {
|
|
29
|
+
if (block._key !== blockOffset.path[0]._key) {
|
|
30
|
+
continue
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!isPortableTextTextBlock(block)) {
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
for (const child of block.children) {
|
|
38
|
+
if (!isPortableTextSpan(child)) {
|
|
39
|
+
continue
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (offsetLeft === 0) {
|
|
43
|
+
selectionPoint = {
|
|
44
|
+
path: [...blockOffset.path, 'children', {_key: child._key}],
|
|
45
|
+
offset: 0,
|
|
46
|
+
}
|
|
47
|
+
break
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (offsetLeft <= child.text.length) {
|
|
51
|
+
selectionPoint = {
|
|
52
|
+
path: [...blockOffset.path, 'children', {_key: child._key}],
|
|
53
|
+
offset: offsetLeft,
|
|
54
|
+
}
|
|
55
|
+
break
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
offsetLeft -= child.text.length
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return selectionPoint
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function spanSelectionPointToBlockOffset({
|
|
66
|
+
value,
|
|
67
|
+
selectionPoint,
|
|
68
|
+
}: {
|
|
69
|
+
value: Array<PortableTextBlock>
|
|
70
|
+
selectionPoint: {
|
|
71
|
+
path: [KeyedSegment, 'children', KeyedSegment]
|
|
72
|
+
offset: number
|
|
73
|
+
}
|
|
74
|
+
}): BlockOffset | undefined {
|
|
75
|
+
let offset = 0
|
|
76
|
+
|
|
77
|
+
for (const block of value) {
|
|
78
|
+
if (block._key !== selectionPoint.path[0]._key) {
|
|
79
|
+
continue
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!isPortableTextTextBlock(block)) {
|
|
83
|
+
continue
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const child of block.children) {
|
|
87
|
+
if (!isPortableTextSpan(child)) {
|
|
88
|
+
continue
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (child._key === selectionPoint.path[2]._key) {
|
|
92
|
+
return {
|
|
93
|
+
path: [{_key: block._key}],
|
|
94
|
+
offset: offset + selectionPoint.offset,
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
offset += child.text.length
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|