@portabletext/editor 1.55.16 → 1.56.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/lib/index.cjs +164 -4
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +164 -4
- package/lib/index.js.map +1 -1
- package/package.json +6 -6
- package/src/behaviors/behavior.core.lists.ts +317 -1
- package/src/behaviors/behavior.core.ts +5 -0
- package/src/internal-utils/terse-pt.test.ts +10 -0
- package/src/operations/behavior.operation.insert.block.ts +12 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.56.0",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -80,15 +80,15 @@
|
|
|
80
80
|
"slate-react": "0.117.3",
|
|
81
81
|
"use-effect-event": "^1.0.2",
|
|
82
82
|
"xstate": "^5.20.0",
|
|
83
|
-
"@portabletext/block-tools": "1.1.
|
|
83
|
+
"@portabletext/block-tools": "1.1.35",
|
|
84
84
|
"@portabletext/patches": "1.1.5"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
87
|
"@portabletext/toolkit": "^2.0.17",
|
|
88
88
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
89
89
|
"@sanity/pkg-utils": "^7.9.0",
|
|
90
|
-
"@sanity/schema": "^3.
|
|
91
|
-
"@sanity/types": "^3.
|
|
90
|
+
"@sanity/schema": "^3.97.1",
|
|
91
|
+
"@sanity/types": "^3.97.1",
|
|
92
92
|
"@testing-library/react": "^16.3.0",
|
|
93
93
|
"@types/debug": "^4.1.12",
|
|
94
94
|
"@types/lodash": "^4.17.16",
|
|
@@ -114,8 +114,8 @@
|
|
|
114
114
|
"racejar": "1.2.9"
|
|
115
115
|
},
|
|
116
116
|
"peerDependencies": {
|
|
117
|
-
"@sanity/schema": "^3.
|
|
118
|
-
"@sanity/types": "^3.
|
|
117
|
+
"@sanity/schema": "^3.97.1",
|
|
118
|
+
"@sanity/types": "^3.97.1",
|
|
119
119
|
"react": "^16.9 || ^17 || ^18 || ^19",
|
|
120
120
|
"rxjs": "^7.8.2"
|
|
121
121
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {isListBlock} from '../internal-utils/parse-blocks'
|
|
1
|
+
import {isListBlock, isTextBlock} from '../internal-utils/parse-blocks'
|
|
2
2
|
import {defaultKeyboardShortcuts} from '../keyboard-shortcuts/default-keyboard-shortcuts'
|
|
3
3
|
import * as selectors from '../selectors'
|
|
4
|
+
import {getBlockEndPoint} from '../utils'
|
|
4
5
|
import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
|
|
5
6
|
import {raise} from './behavior.types.action'
|
|
6
7
|
import {defineBehavior} from './behavior.types.behavior'
|
|
@@ -75,6 +76,104 @@ const unindentListOnBackspace = defineBehavior({
|
|
|
75
76
|
],
|
|
76
77
|
})
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Hitting Delete in an empty list item would delete it by default. Instead,
|
|
81
|
+
* then text block below should be merged into it, preserving the list
|
|
82
|
+
* properties.
|
|
83
|
+
*/
|
|
84
|
+
const mergeTextIntoListOnDelete = defineBehavior({
|
|
85
|
+
on: 'delete.forward',
|
|
86
|
+
guard: ({snapshot}) => {
|
|
87
|
+
const focusListBlock = selectors.getFocusListBlock(snapshot)
|
|
88
|
+
const nextBlock = selectors.getNextBlock(snapshot)
|
|
89
|
+
|
|
90
|
+
if (!focusListBlock || !nextBlock) {
|
|
91
|
+
return false
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!isTextBlock(snapshot.context, nextBlock.node)) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!isEmptyTextBlock(snapshot.context, focusListBlock.node)) {
|
|
99
|
+
return false
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {focusListBlock, nextBlock}
|
|
103
|
+
},
|
|
104
|
+
actions: [
|
|
105
|
+
(_, {nextBlock}) => [
|
|
106
|
+
raise({
|
|
107
|
+
type: 'insert.block',
|
|
108
|
+
block: nextBlock.node,
|
|
109
|
+
placement: 'auto',
|
|
110
|
+
select: 'start',
|
|
111
|
+
}),
|
|
112
|
+
raise({
|
|
113
|
+
type: 'delete.block',
|
|
114
|
+
at: nextBlock.path,
|
|
115
|
+
}),
|
|
116
|
+
],
|
|
117
|
+
],
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Hitting Backspace before an empty list item would delete it by default.
|
|
122
|
+
* Instead, the text block below should be merged into it, preserving the list
|
|
123
|
+
* properties.
|
|
124
|
+
*/
|
|
125
|
+
const mergeTextIntoListOnBackspace = defineBehavior({
|
|
126
|
+
on: 'delete.backward',
|
|
127
|
+
guard: ({snapshot}) => {
|
|
128
|
+
const focusTextBlock = selectors.getFocusTextBlock(snapshot)
|
|
129
|
+
const previousBlock = selectors.getPreviousBlock(snapshot)
|
|
130
|
+
|
|
131
|
+
if (!focusTextBlock || !previousBlock) {
|
|
132
|
+
return false
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (!isListBlock(snapshot.context, previousBlock.node)) {
|
|
136
|
+
return false
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!isEmptyTextBlock(snapshot.context, previousBlock.node)) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const previousBlockEndPoint = getBlockEndPoint({
|
|
144
|
+
context: snapshot.context,
|
|
145
|
+
block: previousBlock,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {focusTextBlock, previousBlockEndPoint}
|
|
149
|
+
},
|
|
150
|
+
actions: [
|
|
151
|
+
(_, {focusTextBlock, previousBlockEndPoint}) => [
|
|
152
|
+
raise({
|
|
153
|
+
type: 'select',
|
|
154
|
+
at: {
|
|
155
|
+
anchor: previousBlockEndPoint,
|
|
156
|
+
focus: previousBlockEndPoint,
|
|
157
|
+
},
|
|
158
|
+
}),
|
|
159
|
+
raise({
|
|
160
|
+
type: 'insert.block',
|
|
161
|
+
block: focusTextBlock.node,
|
|
162
|
+
placement: 'auto',
|
|
163
|
+
select: 'start',
|
|
164
|
+
}),
|
|
165
|
+
raise({
|
|
166
|
+
type: 'delete.block',
|
|
167
|
+
at: focusTextBlock.path,
|
|
168
|
+
}),
|
|
169
|
+
],
|
|
170
|
+
],
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Hitting Enter in an empty list item would create a new list item below by
|
|
175
|
+
* default. Instead, the list properties should be cleared.
|
|
176
|
+
*/
|
|
78
177
|
const clearListOnEnter = defineBehavior({
|
|
79
178
|
on: 'insert.break',
|
|
80
179
|
guard: ({snapshot}) => {
|
|
@@ -102,6 +201,9 @@ const clearListOnEnter = defineBehavior({
|
|
|
102
201
|
],
|
|
103
202
|
})
|
|
104
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Hitting Tab should indent the list item.
|
|
206
|
+
*/
|
|
105
207
|
const indentListOnTab = defineBehavior({
|
|
106
208
|
on: 'keyboard.keydown',
|
|
107
209
|
guard: ({snapshot, event}) => {
|
|
@@ -146,6 +248,9 @@ const indentListOnTab = defineBehavior({
|
|
|
146
248
|
],
|
|
147
249
|
})
|
|
148
250
|
|
|
251
|
+
/**
|
|
252
|
+
* Hitting Shift+Tab should unindent the list item.
|
|
253
|
+
*/
|
|
149
254
|
const unindentListOnShiftTab = defineBehavior({
|
|
150
255
|
on: 'keyboard.keydown',
|
|
151
256
|
guard: ({snapshot, event}) => {
|
|
@@ -192,10 +297,221 @@ const unindentListOnShiftTab = defineBehavior({
|
|
|
192
297
|
],
|
|
193
298
|
})
|
|
194
299
|
|
|
300
|
+
/**
|
|
301
|
+
* An inserted list inherits the `level` from the list item where it's
|
|
302
|
+
* inserted. The entire list tree is adjusted to match the new level.
|
|
303
|
+
*/
|
|
304
|
+
const inheritListLevel = defineBehavior({
|
|
305
|
+
on: 'insert.blocks',
|
|
306
|
+
guard: ({snapshot, event}) => {
|
|
307
|
+
const focusListBlock = selectors.getFocusListBlock(snapshot)
|
|
308
|
+
|
|
309
|
+
if (!focusListBlock) {
|
|
310
|
+
return false
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const firstInsertedBlock = event.blocks.at(0)
|
|
314
|
+
const secondInsertedBlock = event.blocks.at(1)
|
|
315
|
+
const insertedListBlock = isListBlock(snapshot.context, firstInsertedBlock)
|
|
316
|
+
? firstInsertedBlock
|
|
317
|
+
: isListBlock(snapshot.context, secondInsertedBlock)
|
|
318
|
+
? secondInsertedBlock
|
|
319
|
+
: undefined
|
|
320
|
+
|
|
321
|
+
if (!insertedListBlock) {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const levelDifference = focusListBlock.node.level - insertedListBlock.level
|
|
326
|
+
|
|
327
|
+
if (levelDifference === 0) {
|
|
328
|
+
return false
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
return {levelDifference, insertedListBlock}
|
|
332
|
+
},
|
|
333
|
+
actions: [
|
|
334
|
+
({snapshot, event}, {levelDifference, insertedListBlock}) => {
|
|
335
|
+
let adjustLevel = true
|
|
336
|
+
let listStartBlockFound = false
|
|
337
|
+
|
|
338
|
+
return [
|
|
339
|
+
raise({
|
|
340
|
+
...event,
|
|
341
|
+
blocks: event.blocks.map((block) => {
|
|
342
|
+
if (block._key === insertedListBlock._key) {
|
|
343
|
+
listStartBlockFound = true
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (!adjustLevel) {
|
|
347
|
+
return block
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (
|
|
351
|
+
listStartBlockFound &&
|
|
352
|
+
adjustLevel &&
|
|
353
|
+
isListBlock(snapshot.context, block)
|
|
354
|
+
) {
|
|
355
|
+
return {
|
|
356
|
+
...block,
|
|
357
|
+
level: Math.min(
|
|
358
|
+
MAX_LIST_LEVEL,
|
|
359
|
+
Math.max(1, block.level + levelDifference),
|
|
360
|
+
),
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
if (listStartBlockFound) {
|
|
365
|
+
adjustLevel = false
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
return block
|
|
369
|
+
}),
|
|
370
|
+
}),
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
],
|
|
374
|
+
})
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* An inserted list inherits the `listItem` from the list item at the level
|
|
378
|
+
* it's inserted.
|
|
379
|
+
*/
|
|
380
|
+
const inheritListItem = defineBehavior({
|
|
381
|
+
on: 'insert.blocks',
|
|
382
|
+
guard: ({snapshot, event}) => {
|
|
383
|
+
const focusListBlock = selectors.getFocusListBlock(snapshot)
|
|
384
|
+
|
|
385
|
+
if (!focusListBlock) {
|
|
386
|
+
return false
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
if (isEmptyTextBlock(snapshot.context, focusListBlock.node)) {
|
|
390
|
+
return false
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const firstInsertedBlock = event.blocks.at(0)
|
|
394
|
+
const secondInsertedBlock = event.blocks.at(1)
|
|
395
|
+
const insertedListBlock = isListBlock(snapshot.context, firstInsertedBlock)
|
|
396
|
+
? firstInsertedBlock
|
|
397
|
+
: isListBlock(snapshot.context, secondInsertedBlock)
|
|
398
|
+
? secondInsertedBlock
|
|
399
|
+
: undefined
|
|
400
|
+
|
|
401
|
+
if (!insertedListBlock) {
|
|
402
|
+
return false
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if (focusListBlock.node.level !== insertedListBlock.level) {
|
|
406
|
+
return false
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
if (focusListBlock.node.listItem === insertedListBlock.listItem) {
|
|
410
|
+
return false
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
return {listItem: focusListBlock.node.listItem, insertedListBlock}
|
|
414
|
+
},
|
|
415
|
+
actions: [
|
|
416
|
+
({snapshot, event}, {listItem, insertedListBlock}) => {
|
|
417
|
+
let adjustListItem = true
|
|
418
|
+
let listStartBlockFound = false
|
|
419
|
+
|
|
420
|
+
return [
|
|
421
|
+
raise({
|
|
422
|
+
...event,
|
|
423
|
+
blocks: event.blocks.map((block) => {
|
|
424
|
+
if (block._key === insertedListBlock._key) {
|
|
425
|
+
listStartBlockFound = true
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
if (!adjustListItem) {
|
|
429
|
+
return block
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
if (
|
|
433
|
+
listStartBlockFound &&
|
|
434
|
+
adjustListItem &&
|
|
435
|
+
isListBlock(snapshot.context, block)
|
|
436
|
+
) {
|
|
437
|
+
return {
|
|
438
|
+
...block,
|
|
439
|
+
listItem:
|
|
440
|
+
block.level === insertedListBlock.level
|
|
441
|
+
? listItem
|
|
442
|
+
: block.listItem,
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
if (listStartBlockFound) {
|
|
447
|
+
adjustListItem = false
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
return block
|
|
451
|
+
}),
|
|
452
|
+
}),
|
|
453
|
+
]
|
|
454
|
+
},
|
|
455
|
+
],
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* An inserted text block inherits the `listItem` and `level` from the list
|
|
460
|
+
* item where it's inserted.
|
|
461
|
+
*/
|
|
462
|
+
const inheritListProperties = defineBehavior({
|
|
463
|
+
on: 'insert.block',
|
|
464
|
+
guard: ({snapshot, event}) => {
|
|
465
|
+
if (event.placement !== 'auto') {
|
|
466
|
+
return false
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (event.block._type !== snapshot.context.schema.block.name) {
|
|
470
|
+
return false
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (event.block.listItem !== undefined) {
|
|
474
|
+
return false
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
const focusListBlock = selectors.getFocusListBlock(snapshot)
|
|
478
|
+
|
|
479
|
+
if (!focusListBlock) {
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (!isEmptyTextBlock(snapshot.context, focusListBlock.node)) {
|
|
484
|
+
return false
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
level: focusListBlock.node.level,
|
|
489
|
+
listItem: focusListBlock.node.listItem,
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
actions: [
|
|
493
|
+
({event}, {level, listItem}) => [
|
|
494
|
+
raise({
|
|
495
|
+
...event,
|
|
496
|
+
block: {
|
|
497
|
+
...event.block,
|
|
498
|
+
level,
|
|
499
|
+
listItem,
|
|
500
|
+
},
|
|
501
|
+
}),
|
|
502
|
+
],
|
|
503
|
+
],
|
|
504
|
+
})
|
|
505
|
+
|
|
195
506
|
export const coreListBehaviors = {
|
|
196
507
|
clearListOnBackspace,
|
|
197
508
|
unindentListOnBackspace,
|
|
509
|
+
mergeTextIntoListOnDelete,
|
|
510
|
+
mergeTextIntoListOnBackspace,
|
|
198
511
|
clearListOnEnter,
|
|
199
512
|
indentListOnTab,
|
|
200
513
|
unindentListOnShiftTab,
|
|
514
|
+
inheritListLevel,
|
|
515
|
+
inheritListItem,
|
|
516
|
+
inheritListProperties,
|
|
201
517
|
}
|
|
@@ -22,9 +22,14 @@ export const coreBehaviorsConfig = [
|
|
|
22
22
|
coreBlockObjectBehaviors.deletingEmptyTextBlockBeforeBlockObject,
|
|
23
23
|
coreListBehaviors.clearListOnBackspace,
|
|
24
24
|
coreListBehaviors.unindentListOnBackspace,
|
|
25
|
+
coreListBehaviors.mergeTextIntoListOnDelete,
|
|
26
|
+
coreListBehaviors.mergeTextIntoListOnBackspace,
|
|
25
27
|
coreListBehaviors.clearListOnEnter,
|
|
26
28
|
coreListBehaviors.indentListOnTab,
|
|
27
29
|
coreListBehaviors.unindentListOnShiftTab,
|
|
30
|
+
coreListBehaviors.inheritListLevel,
|
|
31
|
+
coreListBehaviors.inheritListItem,
|
|
32
|
+
coreListBehaviors.inheritListProperties,
|
|
28
33
|
coreInsertBreakBehaviors.breakingAtTheEndOfTextBlock,
|
|
29
34
|
coreInsertBreakBehaviors.breakingAtTheStartOfTextBlock,
|
|
30
35
|
coreInsertBreakBehaviors.breakingEntireDocument,
|
|
@@ -79,6 +79,16 @@ test(getTersePt.name, () => {
|
|
|
79
79
|
},
|
|
80
80
|
]),
|
|
81
81
|
).toEqual(['>>#h3:foo'])
|
|
82
|
+
expect(
|
|
83
|
+
getTersePt([
|
|
84
|
+
{
|
|
85
|
+
_key: keyGenerator(),
|
|
86
|
+
_type: 'block',
|
|
87
|
+
children: [{_key: keyGenerator(), _type: 'span', text: 'foo'}],
|
|
88
|
+
style: 'h3',
|
|
89
|
+
},
|
|
90
|
+
]),
|
|
91
|
+
).toEqual(['h3:foo'])
|
|
82
92
|
})
|
|
83
93
|
|
|
84
94
|
test(parseTersePtString.name, () => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../internal-utils/slate-utils'
|
|
12
12
|
import {isEqualToEmptyEditor, toSlateValue} from '../internal-utils/values'
|
|
13
13
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
14
|
+
import {isEmptyTextBlock} from '../utils'
|
|
14
15
|
import type {BehaviorOperationImplementation} from './behavior.operations'
|
|
15
16
|
|
|
16
17
|
export const insertBlockOperationImplementation: BehaviorOperationImplementation<
|
|
@@ -240,6 +241,16 @@ export function insertBlock({
|
|
|
240
241
|
return
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
Transforms.setNodes(
|
|
245
|
+
editor,
|
|
246
|
+
{
|
|
247
|
+
markDefs: [...(endBlock.markDefs ?? []), ...(block.markDefs ?? [])],
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
at: endBlockPath,
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
|
|
243
254
|
if (select === 'end') {
|
|
244
255
|
Transforms.insertFragment(editor, [block], {
|
|
245
256
|
voids: true,
|
|
@@ -290,7 +301,7 @@ export function insertBlock({
|
|
|
290
301
|
Transforms.select(editor, Editor.start(editor, endBlockPath))
|
|
291
302
|
}
|
|
292
303
|
|
|
293
|
-
if (
|
|
304
|
+
if (isEmptyTextBlock({schema}, endBlock)) {
|
|
294
305
|
Transforms.removeNodes(editor, {at: Path.next(endBlockPath)})
|
|
295
306
|
}
|
|
296
307
|
} else if (
|