@portabletext/editor 0.0.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.
Files changed (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,494 @@
1
+ /**
2
+ * This plugin will make the editor support undo/redo on the local state only.
3
+ * The undo/redo steps are rebased against incoming patches since the step occurred.
4
+ */
5
+
6
+ import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, parsePatch} from '@sanity/diff-match-patch'
7
+ import {type ObjectSchemaType, type PortableTextBlock} from '@sanity/types'
8
+ import {flatten, isEqual} from 'lodash'
9
+ import {type Descendant, Editor, Operation, Path, type SelectionOperation, Transforms} from 'slate'
10
+
11
+ import {type PatchObservable, type PortableTextSlateEditor} from '../../types/editor'
12
+ import {type Patch} from '../../types/patch'
13
+ import {debugWithName} from '../../utils/debug'
14
+ import {fromSlateValue} from '../../utils/values'
15
+ import {withPreserveKeys} from '../../utils/withPreserveKeys'
16
+
17
+ const debug = debugWithName('plugin:withUndoRedo')
18
+ const debugVerbose = debug.enabled && false
19
+
20
+ const SAVING = new WeakMap<Editor, boolean | undefined>()
21
+ const REMOTE_PATCHES = new WeakMap<
22
+ Editor,
23
+ {
24
+ patch: Patch
25
+ time: Date
26
+ snapshot: PortableTextBlock[] | undefined
27
+ previousSnapshot: PortableTextBlock[] | undefined
28
+ }[]
29
+ >()
30
+ const UNDO_STEP_LIMIT = 1000
31
+
32
+ const isSaving = (editor: Editor): boolean | undefined => {
33
+ const state = SAVING.get(editor)
34
+ return state === undefined ? true : state
35
+ }
36
+
37
+ export interface Options {
38
+ patches$?: PatchObservable
39
+ readOnly: boolean
40
+ blockSchemaType: ObjectSchemaType
41
+ }
42
+
43
+ const getRemotePatches = (editor: Editor) => {
44
+ if (!REMOTE_PATCHES.get(editor)) {
45
+ REMOTE_PATCHES.set(editor, [])
46
+ }
47
+ return REMOTE_PATCHES.get(editor) || []
48
+ }
49
+
50
+ export function createWithUndoRedo(
51
+ options: Options,
52
+ ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
53
+ const {readOnly, patches$, blockSchemaType} = options
54
+
55
+ return (editor: PortableTextSlateEditor) => {
56
+ let previousSnapshot: PortableTextBlock[] | undefined = fromSlateValue(
57
+ editor.children,
58
+ blockSchemaType.name,
59
+ )
60
+ const remotePatches = getRemotePatches(editor)
61
+ if (patches$) {
62
+ editor.subscriptions.push(() => {
63
+ debug('Subscribing to patches')
64
+ const sub = patches$.subscribe(({patches, snapshot}) => {
65
+ let reset = false
66
+ patches.forEach((patch) => {
67
+ if (!reset && patch.origin !== 'local' && remotePatches) {
68
+ if (patch.type === 'unset' && patch.path.length === 0) {
69
+ debug('Someone else cleared the content, resetting undo/redo history')
70
+ editor.history = {undos: [], redos: []}
71
+ remotePatches.splice(0, remotePatches.length)
72
+ SAVING.set(editor, true)
73
+ reset = true
74
+ return
75
+ }
76
+ remotePatches.push({patch, time: new Date(), snapshot, previousSnapshot})
77
+ }
78
+ })
79
+ previousSnapshot = snapshot
80
+ })
81
+ return () => {
82
+ debug('Unsubscribing to patches')
83
+ sub.unsubscribe()
84
+ }
85
+ })
86
+ }
87
+ editor.history = {undos: [], redos: []}
88
+ const {apply} = editor
89
+ editor.apply = (op: Operation) => {
90
+ if (readOnly) {
91
+ apply(op)
92
+ return
93
+ }
94
+ const {operations, history} = editor
95
+ const {undos} = history
96
+ const step = undos[undos.length - 1]
97
+ const lastOp = step && step.operations && step.operations[step.operations.length - 1]
98
+ const overwrite = shouldOverwrite(op, lastOp)
99
+ const save = isSaving(editor)
100
+
101
+ let merge = true
102
+ if (save) {
103
+ if (!step) {
104
+ merge = false
105
+ } else if (operations.length === 0) {
106
+ merge = shouldMerge(op, lastOp) || overwrite
107
+ }
108
+
109
+ if (step && merge) {
110
+ step.operations.push(op)
111
+ } else {
112
+ const newStep = {
113
+ operations: [...(editor.selection === null ? [] : [createSelectOperation(editor)]), op],
114
+ timestamp: new Date(),
115
+ }
116
+ undos.push(newStep)
117
+ debug('Created new undo step', step)
118
+ }
119
+
120
+ while (undos.length > UNDO_STEP_LIMIT) {
121
+ undos.shift()
122
+ }
123
+
124
+ if (shouldClear(op)) {
125
+ history.redos = []
126
+ }
127
+ }
128
+ apply(op)
129
+ }
130
+
131
+ editor.undo = () => {
132
+ if (readOnly) {
133
+ return
134
+ }
135
+ const {undos} = editor.history
136
+ if (undos.length > 0) {
137
+ const step = undos[undos.length - 1]
138
+ debug('Undoing', step)
139
+ if (step.operations.length > 0) {
140
+ const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp)
141
+ let transformedOperations = step.operations
142
+ otherPatches.forEach((item) => {
143
+ transformedOperations = flatten(
144
+ transformedOperations.map((op) =>
145
+ transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot),
146
+ ),
147
+ )
148
+ })
149
+ try {
150
+ Editor.withoutNormalizing(editor, () => {
151
+ withPreserveKeys(editor, () => {
152
+ withoutSaving(editor, () => {
153
+ transformedOperations
154
+ .map(Operation.inverse)
155
+ .reverse()
156
+ // eslint-disable-next-line max-nested-callbacks
157
+ .forEach((op) => {
158
+ editor.apply(op)
159
+ })
160
+ })
161
+ })
162
+ })
163
+ editor.normalize()
164
+ editor.onChange()
165
+ } catch (err) {
166
+ debug('Could not perform undo step', err)
167
+ remotePatches.splice(0, remotePatches.length)
168
+ Transforms.deselect(editor)
169
+ editor.history = {undos: [], redos: []}
170
+ SAVING.set(editor, true)
171
+ editor.onChange()
172
+ return
173
+ }
174
+ editor.history.redos.push(step)
175
+ editor.history.undos.pop()
176
+ }
177
+ }
178
+ }
179
+
180
+ editor.redo = () => {
181
+ if (readOnly) {
182
+ return
183
+ }
184
+ const {redos} = editor.history
185
+ if (redos.length > 0) {
186
+ const step = redos[redos.length - 1]
187
+ debug('Redoing', step)
188
+ if (step.operations.length > 0) {
189
+ const otherPatches = remotePatches.filter((item) => item.time >= step.timestamp)
190
+ let transformedOperations = step.operations
191
+ otherPatches.forEach((item) => {
192
+ transformedOperations = flatten(
193
+ transformedOperations.map((op) =>
194
+ transformOperation(editor, item.patch, op, item.snapshot, item.previousSnapshot),
195
+ ),
196
+ )
197
+ })
198
+ try {
199
+ Editor.withoutNormalizing(editor, () => {
200
+ withPreserveKeys(editor, () => {
201
+ withoutSaving(editor, () => {
202
+ // eslint-disable-next-line max-nested-callbacks
203
+ transformedOperations.forEach((op) => {
204
+ editor.apply(op)
205
+ })
206
+ })
207
+ })
208
+ })
209
+ editor.normalize()
210
+ editor.onChange()
211
+ } catch (err) {
212
+ debug('Could not perform redo step', err)
213
+ remotePatches.splice(0, remotePatches.length)
214
+ Transforms.deselect(editor)
215
+ editor.history = {undos: [], redos: []}
216
+ SAVING.set(editor, true)
217
+ editor.onChange()
218
+ return
219
+ }
220
+ editor.history.undos.push(step)
221
+ editor.history.redos.pop()
222
+ }
223
+ }
224
+ }
225
+
226
+ // Plugin return
227
+ return editor
228
+ }
229
+ }
230
+
231
+ /**
232
+ * This will adjust the operation paths and offsets according to the
233
+ * remote patches by other editors since the step operations was performed.
234
+ */
235
+ function transformOperation(
236
+ editor: PortableTextSlateEditor,
237
+ patch: Patch,
238
+ operation: Operation,
239
+ snapshot: PortableTextBlock[] | undefined,
240
+ previousSnapshot: PortableTextBlock[] | undefined,
241
+ ): Operation[] {
242
+ if (debugVerbose) {
243
+ debug(`Adjusting '${operation.type}' operation paths for '${patch.type}' patch`)
244
+ debug(`Operation ${JSON.stringify(operation)}`)
245
+ debug(`Patch ${JSON.stringify(patch)}`)
246
+ }
247
+
248
+ const transformedOperation = {...operation}
249
+
250
+ if (patch.type === 'insert' && patch.path.length === 1) {
251
+ const insertBlockIndex = (snapshot || []).findIndex((blk) =>
252
+ isEqual({_key: blk._key}, patch.path[0]),
253
+ )
254
+ debug(
255
+ `Adjusting block path (+${patch.items.length}) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
256
+ )
257
+ return [adjustBlockPath(transformedOperation, patch.items.length, insertBlockIndex)]
258
+ }
259
+
260
+ if (patch.type === 'unset' && patch.path.length === 1) {
261
+ const unsetBlockIndex = (previousSnapshot || []).findIndex((blk) =>
262
+ isEqual({_key: blk._key}, patch.path[0]),
263
+ )
264
+ // If this operation is targeting the same block that got removed, return empty
265
+ if (
266
+ 'path' in transformedOperation &&
267
+ Array.isArray(transformedOperation.path) &&
268
+ transformedOperation.path[0] === unsetBlockIndex
269
+ ) {
270
+ debug('Skipping transformation that targeted removed block')
271
+ return []
272
+ }
273
+ if (debugVerbose) {
274
+ debug(`Selection ${JSON.stringify(editor.selection)}`)
275
+ debug(
276
+ `Adjusting block path (-1) for '${transformedOperation.type}' operation and patch '${patch.type}'`,
277
+ )
278
+ }
279
+ return [adjustBlockPath(transformedOperation, -1, unsetBlockIndex)]
280
+ }
281
+
282
+ // Someone reset the whole value
283
+ if (patch.type === 'unset' && patch.path.length === 0) {
284
+ debug(`Adjusting selection for unset everything patch and ${operation.type} operation`)
285
+ return []
286
+ }
287
+
288
+ if (patch.type === 'diffMatchPatch') {
289
+ const operationTargetBlock = findOperationTargetBlock(editor, transformedOperation)
290
+ if (!operationTargetBlock || !isEqual({_key: operationTargetBlock._key}, patch.path[0])) {
291
+ return [transformedOperation]
292
+ }
293
+ const diffPatches = parsePatch(patch.value)
294
+ diffPatches.forEach((diffPatch) => {
295
+ let adjustOffsetBy = 0
296
+ let changedOffset = diffPatch.utf8Start1
297
+ const {diffs} = diffPatch
298
+ diffs.forEach((diff, index) => {
299
+ const [diffType, text] = diff
300
+ if (diffType === DIFF_INSERT) {
301
+ adjustOffsetBy += text.length
302
+ changedOffset += text.length
303
+ } else if (diffType === DIFF_DELETE) {
304
+ adjustOffsetBy -= text.length
305
+ changedOffset -= text.length
306
+ } else if (diffType === DIFF_EQUAL) {
307
+ // Only up to the point where there are no other changes
308
+ if (!diffs.slice(index).every(([dType]) => dType === DIFF_EQUAL)) {
309
+ changedOffset += text.length
310
+ }
311
+ }
312
+ })
313
+ // Adjust accordingly if someone inserted text in the same node before us
314
+ if (transformedOperation.type === 'insert_text') {
315
+ if (changedOffset < transformedOperation.offset) {
316
+ transformedOperation.offset += adjustOffsetBy
317
+ }
318
+ }
319
+ // Adjust accordingly if someone removed text in the same node before us
320
+ if (transformedOperation.type === 'remove_text') {
321
+ if (changedOffset <= transformedOperation.offset - transformedOperation.text.length) {
322
+ transformedOperation.offset += adjustOffsetBy
323
+ }
324
+ }
325
+ // Adjust set_selection operation's points to new offset
326
+ if (transformedOperation.type === 'set_selection') {
327
+ const currentFocus = transformedOperation.properties?.focus
328
+ ? {...transformedOperation.properties.focus}
329
+ : undefined
330
+ const currentAnchor = transformedOperation?.properties?.anchor
331
+ ? {...transformedOperation.properties.anchor}
332
+ : undefined
333
+ const newFocus = transformedOperation?.newProperties?.focus
334
+ ? {...transformedOperation.newProperties.focus}
335
+ : undefined
336
+ const newAnchor = transformedOperation?.newProperties?.anchor
337
+ ? {...transformedOperation.newProperties.anchor}
338
+ : undefined
339
+ if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
340
+ const points = [currentFocus, currentAnchor, newFocus, newAnchor]
341
+ points.forEach((point) => {
342
+ if (point && changedOffset < point.offset) {
343
+ point.offset += adjustOffsetBy
344
+ }
345
+ })
346
+ if (currentFocus && currentAnchor) {
347
+ transformedOperation.properties = {
348
+ focus: currentFocus,
349
+ anchor: currentAnchor,
350
+ }
351
+ }
352
+ if (newFocus && newAnchor) {
353
+ transformedOperation.newProperties = {
354
+ focus: newFocus,
355
+ anchor: newAnchor,
356
+ }
357
+ }
358
+ }
359
+ }
360
+ })
361
+ return [transformedOperation]
362
+ }
363
+ return [transformedOperation]
364
+ }
365
+ /**
366
+ * Adjust the block path for a operation
367
+ */
368
+ function adjustBlockPath(operation: Operation, level: number, blockIndex: number): Operation {
369
+ const transformedOperation = {...operation}
370
+ if (
371
+ blockIndex >= 0 &&
372
+ transformedOperation.type !== 'set_selection' &&
373
+ Array.isArray(transformedOperation.path) &&
374
+ transformedOperation.path[0] >= blockIndex + level &&
375
+ transformedOperation.path[0] + level > -1
376
+ ) {
377
+ const newPath = [transformedOperation.path[0] + level, ...transformedOperation.path.slice(1)]
378
+ transformedOperation.path = newPath
379
+ }
380
+ if (transformedOperation.type === 'set_selection') {
381
+ const currentFocus = transformedOperation.properties?.focus
382
+ ? {...transformedOperation.properties.focus}
383
+ : undefined
384
+ const currentAnchor = transformedOperation?.properties?.anchor
385
+ ? {...transformedOperation.properties.anchor}
386
+ : undefined
387
+ const newFocus = transformedOperation?.newProperties?.focus
388
+ ? {...transformedOperation.newProperties.focus}
389
+ : undefined
390
+ const newAnchor = transformedOperation?.newProperties?.anchor
391
+ ? {...transformedOperation.newProperties.anchor}
392
+ : undefined
393
+ if ((currentFocus && currentAnchor) || (newFocus && newAnchor)) {
394
+ const points = [currentFocus, currentAnchor, newFocus, newAnchor]
395
+ points.forEach((point) => {
396
+ if (point && point.path[0] >= blockIndex + level && point.path[0] + level > -1) {
397
+ point.path = [point.path[0] + level, ...point.path.slice(1)]
398
+ }
399
+ })
400
+ if (currentFocus && currentAnchor) {
401
+ transformedOperation.properties = {
402
+ focus: currentFocus,
403
+ anchor: currentAnchor,
404
+ }
405
+ }
406
+ if (newFocus && newAnchor) {
407
+ transformedOperation.newProperties = {
408
+ focus: newFocus,
409
+ anchor: newAnchor,
410
+ }
411
+ }
412
+ }
413
+ }
414
+ // // Assign fresh point objects (we don't want to mutate the original ones)
415
+ return transformedOperation
416
+ }
417
+
418
+ // Helper functions for editor.apply above
419
+
420
+ const shouldMerge = (op: Operation, prev: Operation | undefined): boolean => {
421
+ if (op.type === 'set_selection') {
422
+ return true
423
+ }
424
+
425
+ // Text input
426
+ if (
427
+ prev &&
428
+ op.type === 'insert_text' &&
429
+ prev.type === 'insert_text' &&
430
+ op.offset === prev.offset + prev.text.length &&
431
+ Path.equals(op.path, prev.path) &&
432
+ op.text !== ' ' // Tokenize between words
433
+ ) {
434
+ return true
435
+ }
436
+
437
+ // Text deletion
438
+ if (
439
+ prev &&
440
+ op.type === 'remove_text' &&
441
+ prev.type === 'remove_text' &&
442
+ op.offset + op.text.length === prev.offset &&
443
+ Path.equals(op.path, prev.path)
444
+ ) {
445
+ return true
446
+ }
447
+
448
+ // Don't merge
449
+ return false
450
+ }
451
+
452
+ const shouldOverwrite = (op: Operation, prev: Operation | undefined): boolean => {
453
+ if (prev && op.type === 'set_selection' && prev.type === 'set_selection') {
454
+ return true
455
+ }
456
+
457
+ return false
458
+ }
459
+
460
+ const shouldClear = (op: Operation): boolean => {
461
+ if (op.type === 'set_selection') {
462
+ return false
463
+ }
464
+
465
+ return true
466
+ }
467
+
468
+ export function withoutSaving(editor: Editor, fn: () => void): void {
469
+ const prev = isSaving(editor)
470
+ SAVING.set(editor, false)
471
+ fn()
472
+ SAVING.set(editor, prev)
473
+ }
474
+
475
+ function createSelectOperation(editor: Editor): SelectionOperation {
476
+ return {
477
+ type: 'set_selection',
478
+ properties: {...editor.selection},
479
+ newProperties: {...editor.selection},
480
+ }
481
+ }
482
+
483
+ function findOperationTargetBlock(
484
+ editor: PortableTextSlateEditor,
485
+ operation: Operation,
486
+ ): Descendant | undefined {
487
+ let block: Descendant | undefined
488
+ if (operation.type === 'set_selection' && editor.selection) {
489
+ block = editor.children[editor.selection.focus.path[0]]
490
+ } else if ('path' in operation) {
491
+ block = editor.children[operation.path[0]]
492
+ }
493
+ return block
494
+ }
@@ -0,0 +1,81 @@
1
+ import {Editor, Range, Text, Transforms} from 'slate'
2
+
3
+ import {type PortableTextMemberSchemaTypes, type PortableTextSlateEditor} from '../../types/editor'
4
+ import {debugWithName} from '../../utils/debug'
5
+ import {toSlateValue} from '../../utils/values'
6
+ import {type PortableTextEditor} from '../PortableTextEditor'
7
+
8
+ const debug = debugWithName('plugin:withUtils')
9
+
10
+ interface Options {
11
+ schemaTypes: PortableTextMemberSchemaTypes
12
+ keyGenerator: () => string
13
+ portableTextEditor: PortableTextEditor
14
+ }
15
+ /**
16
+ * This plugin makes various util commands available in the editor
17
+ *
18
+ */
19
+ export function createWithUtils({schemaTypes, keyGenerator, portableTextEditor}: Options) {
20
+ return function withUtils(editor: PortableTextSlateEditor): PortableTextSlateEditor {
21
+ // Expands the the selection to wrap around the word the focus is at
22
+ editor.pteExpandToWord = () => {
23
+ const {selection} = editor
24
+ if (selection && !Range.isExpanded(selection)) {
25
+ const [textNode] = Editor.node(editor, selection.focus, {depth: 2})
26
+ if (!textNode || !Text.isText(textNode) || textNode.text.length === 0) {
27
+ debug(`pteExpandToWord: Can't expand to word here`)
28
+ return
29
+ }
30
+ const {focus} = selection
31
+ const focusOffset = focus.offset
32
+ const charsBefore = textNode.text.slice(0, focusOffset)
33
+ const charsAfter = textNode.text.slice(focusOffset, -1)
34
+ const isEmpty = (str: string) => str.match(/\s/g)
35
+ const whiteSpaceBeforeIndex = charsBefore
36
+ .split('')
37
+ .reverse()
38
+ .findIndex((str) => isEmpty(str))
39
+ const newStartOffset =
40
+ whiteSpaceBeforeIndex > -1 ? charsBefore.length - whiteSpaceBeforeIndex : 0
41
+ const whiteSpaceAfterIndex = charsAfter.split('').findIndex((obj) => isEmpty(obj))
42
+ const newEndOffset =
43
+ charsBefore.length +
44
+ (whiteSpaceAfterIndex > -1 ? whiteSpaceAfterIndex : charsAfter.length + 1)
45
+ if (!(newStartOffset === newEndOffset || isNaN(newStartOffset) || isNaN(newEndOffset))) {
46
+ debug('pteExpandToWord: Expanding to focused word')
47
+ Transforms.setSelection(editor, {
48
+ anchor: {...selection.anchor, offset: newStartOffset},
49
+ focus: {...selection.focus, offset: newEndOffset},
50
+ })
51
+ return
52
+ }
53
+ debug(`pteExpandToWord: Can't expand to word here`)
54
+ }
55
+ }
56
+
57
+ editor.pteCreateEmptyBlock = () => {
58
+ const block = toSlateValue(
59
+ [
60
+ {
61
+ _type: schemaTypes.block.name,
62
+ _key: keyGenerator(),
63
+ style: schemaTypes.styles[0].value || 'normal',
64
+ markDefs: [],
65
+ children: [
66
+ {
67
+ _type: 'span',
68
+ _key: keyGenerator(),
69
+ text: '',
70
+ marks: [],
71
+ },
72
+ ],
73
+ },
74
+ ],
75
+ portableTextEditor,
76
+ )[0]
77
+ return block
78
+ }
79
+ return editor
80
+ }
81
+ }