@portabletext/editor 1.15.2 → 1.16.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 (82) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +30 -28
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/selector.get-text-before.cjs +14 -14
  4. package/lib/_chunks-cjs/selector.get-text-before.cjs.map +1 -1
  5. package/lib/_chunks-cjs/{selectors.cjs → selector.is-selection-collapsed.cjs} +8 -8
  6. package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs.map +1 -0
  7. package/lib/_chunks-es/behavior.core.js +12 -10
  8. package/lib/_chunks-es/behavior.core.js.map +1 -1
  9. package/lib/_chunks-es/selector.get-text-before.js +14 -14
  10. package/lib/_chunks-es/selector.get-text-before.js.map +1 -1
  11. package/lib/_chunks-es/{selectors.js → selector.is-selection-collapsed.js} +8 -8
  12. package/lib/_chunks-es/selector.is-selection-collapsed.js.map +1 -0
  13. package/lib/behaviors/index.cjs +35 -35
  14. package/lib/behaviors/index.cjs.map +1 -1
  15. package/lib/behaviors/index.d.cts +40 -45
  16. package/lib/behaviors/index.d.ts +40 -45
  17. package/lib/behaviors/index.js +20 -20
  18. package/lib/behaviors/index.js.map +1 -1
  19. package/lib/index.cjs +868 -542
  20. package/lib/index.cjs.map +1 -1
  21. package/lib/index.d.cts +3791 -4503
  22. package/lib/index.d.ts +3791 -4503
  23. package/lib/index.js +865 -541
  24. package/lib/index.js.map +1 -1
  25. package/lib/selectors/index.cjs +166 -16
  26. package/lib/selectors/index.cjs.map +1 -1
  27. package/lib/selectors/index.d.cts +62 -17
  28. package/lib/selectors/index.d.ts +62 -17
  29. package/lib/selectors/index.js +154 -3
  30. package/lib/selectors/index.js.map +1 -1
  31. package/package.json +11 -11
  32. package/src/behavior-actions/behavior.action-utils.insert-block.ts +3 -5
  33. package/src/behavior-actions/behavior.actions.ts +6 -6
  34. package/src/behavior-actions/behavior.guards.ts +2 -6
  35. package/src/behaviors/behavior.code-editor.ts +5 -9
  36. package/src/behaviors/behavior.core.block-objects.ts +14 -20
  37. package/src/behaviors/behavior.core.lists.ts +13 -19
  38. package/src/behaviors/behavior.links.ts +6 -6
  39. package/src/behaviors/behavior.markdown.ts +27 -40
  40. package/src/behaviors/behavior.types.ts +7 -7
  41. package/src/behaviors/index.ts +1 -0
  42. package/src/editor/Editable.tsx +11 -4
  43. package/src/editor/PortableTextEditor.tsx +4 -5
  44. package/src/editor/{hooks/useSyncValue.test.tsx → __tests__/sync-value.test.tsx} +42 -23
  45. package/src/editor/components/Synchronizer.tsx +53 -80
  46. package/src/{utils/getPortableTextMemberSchemaTypes.ts → editor/create-editor-schema.ts} +3 -3
  47. package/src/editor/create-editor.ts +2 -2
  48. package/src/editor/define-schema.ts +8 -3
  49. package/src/editor/editor-machine.ts +136 -104
  50. package/src/editor/editor-provider.tsx +0 -3
  51. package/src/editor/editor-selector.ts +6 -13
  52. package/src/editor/editor-snapshot.ts +5 -6
  53. package/src/editor/get-active-decorators.ts +20 -0
  54. package/src/editor/mutation-machine.ts +100 -0
  55. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +21 -15
  56. package/src/editor/plugins/createWithMaxBlocks.ts +1 -1
  57. package/src/editor/plugins/createWithPatches.ts +0 -4
  58. package/src/editor/plugins/createWithPlaceholderBlock.ts +1 -1
  59. package/src/editor/plugins/createWithPortableTextSelections.ts +4 -1
  60. package/src/editor/plugins/createWithUndoRedo.ts +3 -3
  61. package/src/editor/sync-machine.ts +657 -0
  62. package/src/editor/withSyncRangeDecorations.ts +17 -5
  63. package/src/index.ts +3 -5
  64. package/src/selectors/_exports/index.ts +1 -0
  65. package/src/selectors/index.ts +10 -4
  66. package/src/selectors/selector.get-active-style.ts +37 -0
  67. package/src/selectors/selector.get-selected-spans.ts +136 -0
  68. package/src/selectors/selector.is-active-annotation.ts +49 -0
  69. package/src/selectors/selector.is-active-decorator.ts +21 -0
  70. package/src/selectors/selector.is-active-list-item.ts +13 -0
  71. package/src/selectors/selector.is-active-style.ts +13 -0
  72. package/src/selectors/selector.is-selection-collapsed.ts +12 -0
  73. package/src/selectors/selector.is-selection-expanded.ts +9 -0
  74. package/src/selectors/selectors.ts +0 -11
  75. package/src/utils/__tests__/operationToPatches.test.ts +2 -2
  76. package/src/utils/__tests__/patchToOperations.test.ts +2 -2
  77. package/src/utils/__tests__/values.test.ts +2 -2
  78. package/src/utils/weakMaps.ts +0 -3
  79. package/src/utils/withChanges.ts +1 -8
  80. package/lib/_chunks-cjs/selectors.cjs.map +0 -1
  81. package/lib/_chunks-es/selectors.js.map +0 -1
  82. package/src/editor/hooks/useSyncValue.ts +0 -426
@@ -1,426 +0,0 @@
1
- import type {PortableTextBlock} from '@sanity/types'
2
- import {debounce, isEqual} from 'lodash'
3
- import {useCallback, useMemo, useRef} from 'react'
4
- import {Editor, Text, Transforms, type Descendant, type Node} from 'slate'
5
- import type {PortableTextSlateEditor} from '../../types/editor'
6
- import {debugWithName} from '../../utils/debug'
7
- import {validateValue} from '../../utils/validateValue'
8
- import {toSlateValue, VOID_CHILD_KEY} from '../../utils/values'
9
- import {
10
- isChangingLocally,
11
- isChangingRemotely,
12
- withRemoteChanges,
13
- } from '../../utils/withChanges'
14
- import {withoutPatching} from '../../utils/withoutPatching'
15
- import type {EditorActor} from '../editor-machine'
16
- import {withoutSaving} from '../plugins/createWithUndoRedo'
17
- import type {PortableTextEditor} from '../PortableTextEditor'
18
-
19
- const debug = debugWithName('hook:useSyncValue')
20
-
21
- /**
22
- * @internal
23
- */
24
- export interface UseSyncValueProps {
25
- editorActor: EditorActor
26
- portableTextEditor: PortableTextEditor
27
- readOnly: boolean
28
- slateEditor: PortableTextSlateEditor
29
- }
30
-
31
- const CURRENT_VALUE = new WeakMap<
32
- PortableTextEditor,
33
- PortableTextBlock[] | undefined
34
- >()
35
-
36
- /**
37
- * Sync value with the editor state
38
- *
39
- * Normally nothing here should apply, and the editor and the real world are perfectly aligned.
40
- *
41
- * Inconsistencies could happen though, so we need to check the editor state when the value changes.
42
- *
43
- * For performance reasons, it makes sense to also do the content validation here, as we already
44
- * iterate over the value and can validate only the new content that is actually changed.
45
- *
46
- * @internal
47
- */
48
- export function useSyncValue(
49
- props: UseSyncValueProps,
50
- ): (
51
- value: PortableTextBlock[] | undefined,
52
- userCallbackFn?: () => void,
53
- ) => void {
54
- const {editorActor, portableTextEditor, readOnly, slateEditor} = props
55
- const schemaTypes = editorActor.getSnapshot().context.schema
56
- const previousValue = useRef<PortableTextBlock[]>(undefined)
57
- const updateValueFunctionRef =
58
- useRef<(value: PortableTextBlock[] | undefined) => void>(undefined)
59
-
60
- const updateFromCurrentValue = useCallback(() => {
61
- const currentValue = CURRENT_VALUE.get(portableTextEditor)
62
- if (previousValue.current === currentValue) {
63
- debug('Value is the same object as previous, not need to sync')
64
- return
65
- }
66
- if (updateValueFunctionRef.current && currentValue) {
67
- debug('Updating the value debounced')
68
- updateValueFunctionRef.current(currentValue)
69
- }
70
- }, [portableTextEditor])
71
- const updateValueDebounced = useMemo(
72
- () =>
73
- debounce(updateFromCurrentValue, 1000, {trailing: true, leading: false}),
74
- [updateFromCurrentValue],
75
- )
76
-
77
- return useMemo(() => {
78
- const updateFunction = (value: PortableTextBlock[] | undefined) => {
79
- CURRENT_VALUE.set(portableTextEditor, value)
80
- const isProcessingLocalChanges = isChangingLocally(slateEditor)
81
- const isProcessingRemoteChanges = isChangingRemotely(slateEditor)
82
- if (!readOnly) {
83
- if (isProcessingLocalChanges) {
84
- debug('Has local changes, not syncing value right now')
85
- updateValueDebounced()
86
- return
87
- }
88
- if (isProcessingRemoteChanges) {
89
- debug('Has remote changes, not syncing value right now')
90
- updateValueDebounced()
91
- return
92
- }
93
- }
94
-
95
- let isChanged = false
96
- let isValid = true
97
-
98
- const hadSelection = !!slateEditor.selection
99
-
100
- // If empty value, remove everything in the editor and insert a placeholder block
101
- if (!value || value.length === 0) {
102
- debug('Value is empty')
103
- Editor.withoutNormalizing(slateEditor, () => {
104
- withoutSaving(slateEditor, () => {
105
- withoutPatching(slateEditor, () => {
106
- if (hadSelection) {
107
- Transforms.deselect(slateEditor)
108
- }
109
- const childrenLength = slateEditor.children.length
110
- slateEditor.children.forEach((_, index) => {
111
- Transforms.removeNodes(slateEditor, {
112
- at: [childrenLength - 1 - index],
113
- })
114
- })
115
- Transforms.insertNodes(
116
- slateEditor,
117
- slateEditor.pteCreateTextBlock({decorators: []}),
118
- {at: [0]},
119
- )
120
- // Add a new selection in the top of the document
121
- if (hadSelection) {
122
- Transforms.select(slateEditor, [0, 0])
123
- }
124
- })
125
- })
126
- })
127
- isChanged = true
128
- }
129
- // Remove, replace or add nodes according to what is changed.
130
- if (value && value.length > 0) {
131
- const slateValueFromProps = toSlateValue(value, {
132
- schemaTypes,
133
- })
134
- Editor.withoutNormalizing(slateEditor, () => {
135
- withRemoteChanges(slateEditor, () => {
136
- withoutSaving(slateEditor, () => {
137
- withoutPatching(slateEditor, () => {
138
- const childrenLength = slateEditor.children.length
139
- // Remove blocks that have become superfluous
140
- if (slateValueFromProps.length < childrenLength) {
141
- for (
142
- let i = childrenLength - 1;
143
- i > slateValueFromProps.length - 1;
144
- i--
145
- ) {
146
- Transforms.removeNodes(slateEditor, {
147
- at: [i],
148
- })
149
- }
150
- isChanged = true
151
- }
152
- // Go through all of the blocks and see if they need to be updated
153
- slateValueFromProps.forEach(
154
- (currentBlock, currentBlockIndex) => {
155
- const oldBlock = slateEditor.children[currentBlockIndex]
156
- const hasChanges =
157
- oldBlock && !isEqual(currentBlock, oldBlock)
158
- if (hasChanges && isValid) {
159
- const validationValue = [value[currentBlockIndex]]
160
- const validation = validateValue(
161
- validationValue,
162
- schemaTypes,
163
- editorActor.getSnapshot().context.keyGenerator,
164
- )
165
- // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
166
- if (
167
- !validation.valid &&
168
- validation.resolution?.autoResolve &&
169
- validation.resolution?.patches.length > 0
170
- ) {
171
- // Only apply auto resolution if the value has been populated before and is different from the last one.
172
- if (
173
- !readOnly &&
174
- previousValue.current &&
175
- previousValue.current !== value
176
- ) {
177
- // Give a console warning about the fact that it did an auto resolution
178
- console.warn(
179
- `${validation.resolution.action} for block with _key '${validationValue[0]._key}'. ${validation.resolution?.description}`,
180
- )
181
- validation.resolution.patches.forEach((patch) => {
182
- editorActor.send({type: 'patch', patch})
183
- })
184
- }
185
- }
186
- if (
187
- validation.valid ||
188
- validation.resolution?.autoResolve
189
- ) {
190
- if (oldBlock._key === currentBlock._key) {
191
- if (debug.enabled)
192
- debug('Updating block', oldBlock, currentBlock)
193
- _updateBlock(
194
- slateEditor,
195
- currentBlock,
196
- oldBlock,
197
- currentBlockIndex,
198
- )
199
- } else {
200
- if (debug.enabled)
201
- debug('Replacing block', oldBlock, currentBlock)
202
- _replaceBlock(
203
- slateEditor,
204
- currentBlock,
205
- currentBlockIndex,
206
- )
207
- }
208
- isChanged = true
209
- } else {
210
- editorActor.send({
211
- type: 'invalid value',
212
- resolution: validation.resolution,
213
- value,
214
- })
215
- isValid = false
216
- }
217
- }
218
- if (!oldBlock && isValid) {
219
- const validationValue = [value[currentBlockIndex]]
220
- const validation = validateValue(
221
- validationValue,
222
- schemaTypes,
223
- editorActor.getSnapshot().context.keyGenerator,
224
- )
225
- if (debug.enabled)
226
- debug(
227
- 'Validating and inserting new block in the end of the value',
228
- currentBlock,
229
- )
230
- if (
231
- validation.valid ||
232
- validation.resolution?.autoResolve
233
- ) {
234
- Transforms.insertNodes(slateEditor, currentBlock, {
235
- at: [currentBlockIndex],
236
- })
237
- } else {
238
- debug('Invalid', validation)
239
- editorActor.send({
240
- type: 'invalid value',
241
- resolution: validation.resolution,
242
- value,
243
- })
244
- isValid = false
245
- }
246
- }
247
- },
248
- )
249
- })
250
- })
251
- })
252
- })
253
- }
254
-
255
- if (!isValid) {
256
- debug('Invalid value, returning')
257
- return
258
- }
259
- if (isChanged) {
260
- debug('Server value changed, syncing editor')
261
- try {
262
- slateEditor.onChange()
263
- } catch (err) {
264
- console.error(err)
265
- editorActor.send({
266
- type: 'invalid value',
267
- resolution: null,
268
- value,
269
- })
270
- return
271
- }
272
- if (hadSelection && !slateEditor.selection) {
273
- Transforms.select(slateEditor, {
274
- anchor: {path: [0, 0], offset: 0},
275
- focus: {path: [0, 0], offset: 0},
276
- })
277
- slateEditor.onChange()
278
- }
279
- editorActor.send({type: 'value changed', value})
280
- } else {
281
- debug('Server value and editor value is equal, no need to sync.')
282
- }
283
- previousValue.current = value
284
- }
285
- updateValueFunctionRef.current = updateFunction
286
- return updateFunction
287
- }, [
288
- editorActor,
289
- portableTextEditor,
290
- readOnly,
291
- schemaTypes,
292
- slateEditor,
293
- updateValueDebounced,
294
- ])
295
- }
296
-
297
- /**
298
- * This code is moved out of the above algorithm to keep complexity down.
299
- * @internal
300
- */
301
- function _replaceBlock(
302
- slateEditor: PortableTextSlateEditor,
303
- currentBlock: Descendant,
304
- currentBlockIndex: number,
305
- ) {
306
- // While replacing the block and the current selection focus is on the replaced block,
307
- // temporarily deselect the editor then optimistically try to restore the selection afterwards.
308
- const currentSelection = slateEditor.selection
309
- const selectionFocusOnBlock =
310
- currentSelection && currentSelection.focus.path[0] === currentBlockIndex
311
- if (selectionFocusOnBlock) {
312
- Transforms.deselect(slateEditor)
313
- }
314
- Transforms.removeNodes(slateEditor, {at: [currentBlockIndex]})
315
- Transforms.insertNodes(slateEditor, currentBlock, {at: [currentBlockIndex]})
316
- slateEditor.onChange()
317
- if (selectionFocusOnBlock) {
318
- Transforms.select(slateEditor, currentSelection)
319
- }
320
- }
321
-
322
- /**
323
- * This code is moved out of the above algorithm to keep complexity down.
324
- * @internal
325
- */
326
- function _updateBlock(
327
- slateEditor: PortableTextSlateEditor,
328
- currentBlock: Descendant,
329
- oldBlock: Descendant,
330
- currentBlockIndex: number,
331
- ) {
332
- // Update the root props on the block
333
- Transforms.setNodes(slateEditor, currentBlock as Partial<Node>, {
334
- at: [currentBlockIndex],
335
- })
336
- // Text block's need to have their children updated as well (setNode does not target a node's children)
337
- if (
338
- slateEditor.isTextBlock(currentBlock) &&
339
- slateEditor.isTextBlock(oldBlock)
340
- ) {
341
- const oldBlockChildrenLength = oldBlock.children.length
342
- if (currentBlock.children.length < oldBlockChildrenLength) {
343
- // Remove any children that have become superfluous
344
- Array.from(
345
- Array(oldBlockChildrenLength - currentBlock.children.length),
346
- ).forEach((_, index) => {
347
- const childIndex = oldBlockChildrenLength - 1 - index
348
- if (childIndex > 0) {
349
- debug('Removing child')
350
- Transforms.removeNodes(slateEditor, {
351
- at: [currentBlockIndex, childIndex],
352
- })
353
- }
354
- })
355
- }
356
- currentBlock.children.forEach(
357
- (currentBlockChild, currentBlockChildIndex) => {
358
- const oldBlockChild = oldBlock.children[currentBlockChildIndex]
359
- const isChildChanged = !isEqual(currentBlockChild, oldBlockChild)
360
- const isTextChanged = !isEqual(
361
- currentBlockChild.text,
362
- oldBlockChild?.text,
363
- )
364
- const path = [currentBlockIndex, currentBlockChildIndex]
365
- if (isChildChanged) {
366
- // Update if this is the same child
367
- if (currentBlockChild._key === oldBlockChild?._key) {
368
- debug('Updating changed child', currentBlockChild, oldBlockChild)
369
- Transforms.setNodes(
370
- slateEditor,
371
- currentBlockChild as Partial<Node>,
372
- {
373
- at: path,
374
- },
375
- )
376
- const isSpanNode =
377
- Text.isText(currentBlockChild) &&
378
- currentBlockChild._type === 'span' &&
379
- Text.isText(oldBlockChild) &&
380
- oldBlockChild._type === 'span'
381
- if (isSpanNode && isTextChanged) {
382
- Transforms.delete(slateEditor, {
383
- at: {
384
- focus: {path, offset: 0},
385
- anchor: {path, offset: oldBlockChild.text.length},
386
- },
387
- })
388
- Transforms.insertText(slateEditor, currentBlockChild.text, {
389
- at: path,
390
- })
391
- slateEditor.onChange()
392
- } else if (!isSpanNode) {
393
- // If it's a inline block, also update the void text node key
394
- debug('Updating changed inline object child', currentBlockChild)
395
- Transforms.setNodes(
396
- slateEditor,
397
- {_key: VOID_CHILD_KEY},
398
- {
399
- at: [...path, 0],
400
- voids: true,
401
- },
402
- )
403
- }
404
- // Replace the child if _key's are different
405
- } else if (oldBlockChild) {
406
- debug('Replacing child', currentBlockChild)
407
- Transforms.removeNodes(slateEditor, {
408
- at: [currentBlockIndex, currentBlockChildIndex],
409
- })
410
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
411
- at: [currentBlockIndex, currentBlockChildIndex],
412
- })
413
- slateEditor.onChange()
414
- // Insert it if it didn't exist before
415
- } else if (!oldBlockChild) {
416
- debug('Inserting new child', currentBlockChild)
417
- Transforms.insertNodes(slateEditor, currentBlockChild as Node, {
418
- at: [currentBlockIndex, currentBlockChildIndex],
419
- })
420
- slateEditor.onChange()
421
- }
422
- }
423
- },
424
- )
425
- }
426
- }