@portabletext/editor 1.11.2 → 1.12.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/README.md +11 -0
- package/lib/index.d.mts +26 -7
- package/lib/index.d.ts +26 -7
- package/lib/index.esm.js +317 -134
- package/lib/index.esm.js.map +1 -1
- package/lib/index.js +316 -133
- package/lib/index.js.map +1 -1
- package/lib/index.mjs +317 -134
- package/lib/index.mjs.map +1 -1
- package/package.json +6 -6
- package/src/editor/behavior/behavior.action-utils.insert-block.ts +61 -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 +149 -62
- 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.ts +13 -2
- package/src/editor/plugins/createWithEditableAPI.ts +22 -87
- package/src/index.ts +1 -0
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
import {isPortableTextSpan} from '@portabletext/toolkit'
|
|
2
|
+
import {isPortableTextTextBlock} from '@sanity/types'
|
|
2
3
|
import type {PortableTextMemberSchemaTypes} from '../../types/editor'
|
|
3
4
|
import {defineBehavior} from './behavior.types'
|
|
4
5
|
import {
|
|
6
|
+
getFocusBlock,
|
|
5
7
|
getFocusSpan,
|
|
6
8
|
getFocusTextBlock,
|
|
9
|
+
getTextBlockText,
|
|
7
10
|
selectionIsCollapsed,
|
|
8
11
|
} from './behavior.utils'
|
|
12
|
+
import {spanSelectionPointToBlockOffset} from './behavior.utils.block-offset'
|
|
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,13 +136,13 @@ 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
|
|
|
@@ -133,7 +152,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
133
152
|
.join('')
|
|
134
153
|
|
|
135
154
|
if (onlyText && blockText === `${hrCharacter}${hrCharacter}`) {
|
|
136
|
-
return {
|
|
155
|
+
return {hrObject, focusBlock, hrCharacter}
|
|
137
156
|
}
|
|
138
157
|
|
|
139
158
|
return false
|
|
@@ -145,31 +164,71 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
145
164
|
text: hrCharacter,
|
|
146
165
|
},
|
|
147
166
|
],
|
|
148
|
-
(_, {
|
|
167
|
+
(_, {hrObject, focusBlock}) => [
|
|
149
168
|
{
|
|
150
169
|
type: 'insert block object',
|
|
151
|
-
|
|
170
|
+
placement: 'after',
|
|
171
|
+
blockObject: hrObject,
|
|
152
172
|
},
|
|
153
173
|
{
|
|
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
|
-
},
|
|
174
|
+
type: 'delete block',
|
|
175
|
+
blockPath: focusBlock.path,
|
|
165
176
|
},
|
|
166
177
|
{
|
|
167
178
|
type: 'insert text block',
|
|
168
|
-
|
|
179
|
+
placement: 'after',
|
|
169
180
|
},
|
|
170
181
|
],
|
|
171
182
|
],
|
|
172
183
|
})
|
|
184
|
+
const automaticHrOnPaste = defineBehavior({
|
|
185
|
+
on: 'paste',
|
|
186
|
+
guard: ({context, event}) => {
|
|
187
|
+
const text = event.clipboardData.getData('text/plain')
|
|
188
|
+
const hrRegExp = /^(---)$|(___)$|(\*\*\*)$/gm
|
|
189
|
+
const hrCharacters = text.match(hrRegExp)?.[0]
|
|
190
|
+
const hrObject = config.horizontalRuleObject?.({
|
|
191
|
+
schema: context.schema,
|
|
192
|
+
})
|
|
193
|
+
const focusBlock = getFocusBlock(context)
|
|
194
|
+
|
|
195
|
+
if (!hrCharacters || !hrObject || !focusBlock) {
|
|
196
|
+
return false
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {hrCharacters, hrObject, focusBlock}
|
|
200
|
+
},
|
|
201
|
+
actions: [
|
|
202
|
+
(_, {hrCharacters}) => [
|
|
203
|
+
{
|
|
204
|
+
type: 'insert text',
|
|
205
|
+
text: hrCharacters,
|
|
206
|
+
},
|
|
207
|
+
],
|
|
208
|
+
(_, {hrObject, focusBlock}) =>
|
|
209
|
+
isPortableTextTextBlock(focusBlock.node)
|
|
210
|
+
? [
|
|
211
|
+
{
|
|
212
|
+
type: 'insert text block',
|
|
213
|
+
textBlock: {children: focusBlock.node.children},
|
|
214
|
+
placement: 'after',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
type: 'insert block object',
|
|
218
|
+
blockObject: hrObject,
|
|
219
|
+
placement: 'after',
|
|
220
|
+
},
|
|
221
|
+
{type: 'delete block', blockPath: focusBlock.path},
|
|
222
|
+
]
|
|
223
|
+
: [
|
|
224
|
+
{
|
|
225
|
+
type: 'insert block object',
|
|
226
|
+
blockObject: hrObject,
|
|
227
|
+
placement: 'after',
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
],
|
|
231
|
+
})
|
|
173
232
|
const automaticHeadingOnSpace = defineBehavior({
|
|
174
233
|
on: 'insert text',
|
|
175
234
|
guard: ({context, event}) => {
|
|
@@ -187,11 +246,28 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
187
246
|
return false
|
|
188
247
|
}
|
|
189
248
|
|
|
190
|
-
const
|
|
249
|
+
const blockOffset = spanSelectionPointToBlockOffset({
|
|
250
|
+
value: context.value,
|
|
251
|
+
selectionPoint: {
|
|
252
|
+
path: [
|
|
253
|
+
{_key: focusTextBlock.node._key},
|
|
254
|
+
'children',
|
|
255
|
+
{_key: focusSpan.node._key},
|
|
256
|
+
],
|
|
257
|
+
offset: context.selection.focus.offset,
|
|
258
|
+
},
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
if (!blockOffset) {
|
|
262
|
+
return false
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const blockText = getTextBlockText(focusTextBlock.node)
|
|
266
|
+
const markdownHeadingSearch = /^#+/.exec(blockText)
|
|
191
267
|
const level = markdownHeadingSearch
|
|
192
268
|
? markdownHeadingSearch[0].length
|
|
193
269
|
: undefined
|
|
194
|
-
const caretAtTheEndOfHeading =
|
|
270
|
+
const caretAtTheEndOfHeading = blockOffset.offset === level
|
|
195
271
|
|
|
196
272
|
if (!caretAtTheEndOfHeading) {
|
|
197
273
|
return false
|
|
@@ -205,7 +281,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
205
281
|
if (level !== undefined && style !== undefined) {
|
|
206
282
|
return {
|
|
207
283
|
focusTextBlock,
|
|
208
|
-
focusSpan,
|
|
209
284
|
style: style,
|
|
210
285
|
level,
|
|
211
286
|
}
|
|
@@ -220,7 +295,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
220
295
|
text: ' ',
|
|
221
296
|
},
|
|
222
297
|
],
|
|
223
|
-
(_, {focusTextBlock,
|
|
298
|
+
(_, {focusTextBlock, style, level}) => [
|
|
224
299
|
{
|
|
225
300
|
type: 'unset block',
|
|
226
301
|
props: ['listItem', 'level'],
|
|
@@ -232,16 +307,14 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
232
307
|
paths: [focusTextBlock.path],
|
|
233
308
|
},
|
|
234
309
|
{
|
|
235
|
-
type: 'delete',
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
offset: level + 1,
|
|
244
|
-
},
|
|
310
|
+
type: 'delete text',
|
|
311
|
+
anchor: {
|
|
312
|
+
path: focusTextBlock.path,
|
|
313
|
+
offset: 0,
|
|
314
|
+
},
|
|
315
|
+
focus: {
|
|
316
|
+
path: focusTextBlock.path,
|
|
317
|
+
offset: level + 1,
|
|
245
318
|
},
|
|
246
319
|
},
|
|
247
320
|
],
|
|
@@ -301,12 +374,29 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
301
374
|
return false
|
|
302
375
|
}
|
|
303
376
|
|
|
377
|
+
const blockOffset = spanSelectionPointToBlockOffset({
|
|
378
|
+
value: context.value,
|
|
379
|
+
selectionPoint: {
|
|
380
|
+
path: [
|
|
381
|
+
{_key: focusTextBlock.node._key},
|
|
382
|
+
'children',
|
|
383
|
+
{_key: focusSpan.node._key},
|
|
384
|
+
],
|
|
385
|
+
offset: context.selection.focus.offset,
|
|
386
|
+
},
|
|
387
|
+
})
|
|
388
|
+
|
|
389
|
+
if (!blockOffset) {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const blockText = getTextBlockText(focusTextBlock.node)
|
|
304
394
|
const defaultStyle = config.defaultStyle?.({schema: context.schema})
|
|
305
|
-
const looksLikeUnorderedList = /^(-|\*)/.test(
|
|
395
|
+
const looksLikeUnorderedList = /^(-|\*)/.test(blockText)
|
|
306
396
|
const unorderedListStyle = config.unorderedListStyle?.({
|
|
307
397
|
schema: context.schema,
|
|
308
398
|
})
|
|
309
|
-
const caretAtTheEndOfUnorderedList =
|
|
399
|
+
const caretAtTheEndOfUnorderedList = blockOffset.offset === 1
|
|
310
400
|
|
|
311
401
|
if (
|
|
312
402
|
defaultStyle &&
|
|
@@ -316,7 +406,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
316
406
|
) {
|
|
317
407
|
return {
|
|
318
408
|
focusTextBlock,
|
|
319
|
-
focusSpan,
|
|
320
409
|
listItem: unorderedListStyle,
|
|
321
410
|
listItemLength: 1,
|
|
322
411
|
style: defaultStyle,
|
|
@@ -337,7 +426,6 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
337
426
|
) {
|
|
338
427
|
return {
|
|
339
428
|
focusTextBlock,
|
|
340
|
-
focusSpan,
|
|
341
429
|
listItem: orderedListStyle,
|
|
342
430
|
listItemLength: 2,
|
|
343
431
|
style: defaultStyle,
|
|
@@ -353,7 +441,7 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
353
441
|
text: ' ',
|
|
354
442
|
},
|
|
355
443
|
],
|
|
356
|
-
(_, {focusTextBlock,
|
|
444
|
+
(_, {focusTextBlock, style, listItem, listItemLength}) => [
|
|
357
445
|
{
|
|
358
446
|
type: 'set block',
|
|
359
447
|
listItem,
|
|
@@ -362,16 +450,14 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
362
450
|
paths: [focusTextBlock.path],
|
|
363
451
|
},
|
|
364
452
|
{
|
|
365
|
-
type: 'delete',
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
offset: listItemLength + 1,
|
|
374
|
-
},
|
|
453
|
+
type: 'delete text',
|
|
454
|
+
anchor: {
|
|
455
|
+
path: focusTextBlock.path,
|
|
456
|
+
offset: 0,
|
|
457
|
+
},
|
|
458
|
+
focus: {
|
|
459
|
+
path: focusTextBlock.path,
|
|
460
|
+
offset: listItemLength + 1,
|
|
375
461
|
},
|
|
376
462
|
},
|
|
377
463
|
],
|
|
@@ -380,8 +466,9 @@ export function createMarkdownBehaviors(config: MarkdownBehaviorsConfig) {
|
|
|
380
466
|
|
|
381
467
|
const markdownBehaviors = [
|
|
382
468
|
automaticBlockquoteOnSpace,
|
|
383
|
-
automaticBreak,
|
|
384
469
|
automaticHeadingOnSpace,
|
|
470
|
+
automaticHr,
|
|
471
|
+
automaticHrOnPaste,
|
|
385
472
|
clearStyleOnBackspace,
|
|
386
473
|
automaticListOnSpace,
|
|
387
474
|
]
|
|
@@ -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'
|
|
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'
|
|
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
|
+
}
|
|
@@ -202,6 +202,17 @@ export function getNextBlock(
|
|
|
202
202
|
return undefined
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
export function isEmptyTextBlock(block:
|
|
206
|
-
|
|
205
|
+
export function isEmptyTextBlock(block: PortableTextBlock) {
|
|
206
|
+
if (!isPortableTextTextBlock(block)) {
|
|
207
|
+
return false
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const onlyText = block.children.every(isPortableTextSpan)
|
|
211
|
+
const blockText = getTextBlockText(block)
|
|
212
|
+
|
|
213
|
+
return onlyText && blockText === ''
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function getTextBlockText(block: PortableTextTextBlock) {
|
|
217
|
+
return block.children.map((child) => child.text ?? '').join('')
|
|
207
218
|
}
|