@portabletext/editor 1.44.4 → 1.44.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.44.4",
3
+ "version": "1.44.6",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -73,7 +73,7 @@
73
73
  "get-random-values-esm": "^1.0.2",
74
74
  "lodash": "^4.17.21",
75
75
  "lodash.startcase": "^4.4.0",
76
- "react-compiler-runtime": "19.0.0-beta-3229e95-20250315",
76
+ "react-compiler-runtime": "19.0.0-beta-e993439-20250328",
77
77
  "slate": "0.112.0",
78
78
  "slate-dom": "^0.112.2",
79
79
  "slate-react": "0.112.1",
@@ -86,8 +86,8 @@
86
86
  "@portabletext/toolkit": "^2.0.17",
87
87
  "@sanity/diff-match-patch": "^3.2.0",
88
88
  "@sanity/pkg-utils": "^7.2.2",
89
- "@sanity/schema": "^3.81.0",
90
- "@sanity/types": "^3.81.0",
89
+ "@sanity/schema": "^3.82.0",
90
+ "@sanity/types": "^3.82.0",
91
91
  "@testing-library/jest-dom": "^6.6.3",
92
92
  "@testing-library/react": "^16.2.0",
93
93
  "@types/debug": "^4.1.12",
@@ -100,9 +100,9 @@
100
100
  "@vitejs/plugin-react": "^4.3.4",
101
101
  "@vitest/browser": "^3.1.1",
102
102
  "@vitest/coverage-istanbul": "^3.1.1",
103
- "babel-plugin-react-compiler": "19.0.0-beta-3229e95-20250315",
103
+ "babel-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
104
104
  "eslint": "8.57.1",
105
- "eslint-plugin-react-compiler": "19.0.0-beta-3229e95-20250315",
105
+ "eslint-plugin-react-compiler": "19.0.0-beta-e993439-20250328",
106
106
  "eslint-plugin-react-hooks": "experimental",
107
107
  "jsdom": "^26.0.0",
108
108
  "react": "^19.1.0",
@@ -115,8 +115,8 @@
115
115
  "racejar": "1.2.3"
116
116
  },
117
117
  "peerDependencies": {
118
- "@sanity/schema": "^3.81.0",
119
- "@sanity/types": "^3.81.0",
118
+ "@sanity/schema": "^3.82.0",
119
+ "@sanity/types": "^3.82.0",
120
120
  "react": "^16.9 || ^17 || ^18 || ^19",
121
121
  "rxjs": "^7.8.2"
122
122
  },
@@ -1,5 +1,5 @@
1
- import {useSelector} from '@xstate/react'
2
- import {isEqual, noop} from 'lodash'
1
+ import {useActorRef, useSelector} from '@xstate/react'
2
+ import {noop} from 'lodash'
3
3
  import {
4
4
  forwardRef,
5
5
  useCallback,
@@ -16,16 +16,7 @@ import {
16
16
  type MutableRefObject,
17
17
  type TextareaHTMLAttributes,
18
18
  } from 'react'
19
- import {
20
- Editor,
21
- Path,
22
- Range as SlateRange,
23
- Transforms,
24
- type BaseRange,
25
- type NodeEntry,
26
- type Operation,
27
- type Text,
28
- } from 'slate'
19
+ import {Editor, Transforms, type Text} from 'slate'
29
20
  import {
30
21
  ReactEditor,
31
22
  Editable as SlateEditable,
@@ -38,11 +29,11 @@ import {debugWithName} from '../internal-utils/debug'
38
29
  import {getDragSelection} from '../internal-utils/drag-selection'
39
30
  import {getEventPosition} from '../internal-utils/event-position'
40
31
  import {parseBlocks} from '../internal-utils/parse-blocks'
41
- import {moveRangeByOperation, toSlateRange} from '../internal-utils/ranges'
32
+ import {toSlateRange} from '../internal-utils/ranges'
42
33
  import {normalizeSelection} from '../internal-utils/selection'
43
34
  import {getSelectionDomNodes} from '../internal-utils/selection-elements'
44
35
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
45
- import {fromSlateValue, isEqualToEmptyEditor} from '../internal-utils/values'
36
+ import {fromSlateValue} from '../internal-utils/values'
46
37
  import * as selectors from '../selectors'
47
38
  import type {
48
39
  EditorSelection,
@@ -68,7 +59,10 @@ import {getEditorSnapshot} from './editor-selector'
68
59
  import {usePortableTextEditor} from './hooks/usePortableTextEditor'
69
60
  import {createWithHotkeys} from './plugins/createWithHotKeys'
70
61
  import {PortableTextEditor} from './PortableTextEditor'
71
- import {withSyncRangeDecorations} from './withSyncRangeDecorations'
62
+ import {
63
+ createDecorate,
64
+ rangeDecorationsMachine,
65
+ } from './range-decorations-machine'
72
66
 
73
67
  const debug = debugWithName('component:Editable')
74
68
 
@@ -80,10 +74,6 @@ const PLACEHOLDER_STYLE: CSSProperties = {
80
74
  right: 0,
81
75
  }
82
76
 
83
- interface BaseRangeWithDecoration extends BaseRange {
84
- rangeDecoration: RangeDecoration
85
- }
86
-
87
77
  /**
88
78
  * @public
89
79
  */
@@ -169,9 +159,6 @@ export const PortableTextEditable = forwardRef<
169
159
  null,
170
160
  )
171
161
  const [hasInvalidValue, setHasInvalidValue] = useState(false)
172
- const [rangeDecorationState, setRangeDecorationsState] = useState<
173
- BaseRangeWithDecoration[]
174
- >([])
175
162
 
176
163
  // Forward ref to parent component
177
164
  useImperativeHandle<HTMLDivElement | null, HTMLDivElement | null>(
@@ -179,8 +166,6 @@ export const PortableTextEditable = forwardRef<
179
166
  () => ref.current,
180
167
  )
181
168
 
182
- const rangeDecorationsRef = useRef(rangeDecorations)
183
-
184
169
  const editorActor = useContext(EditorActorContext)
185
170
  const readOnly = useSelector(editorActor, (s) =>
186
171
  s.matches({'edit mode': 'read only'}),
@@ -188,6 +173,33 @@ export const PortableTextEditable = forwardRef<
188
173
  const schemaTypes = useSelector(editorActor, (s) => s.context.schema)
189
174
  const slateEditor = useSlate()
190
175
 
176
+ const rangeDecorationsActor = useActorRef(rangeDecorationsMachine, {
177
+ input: {
178
+ readOnly,
179
+ slateEditor,
180
+ schema: schemaTypes,
181
+ rangeDecorations: rangeDecorations ?? [],
182
+ },
183
+ })
184
+ const decorate = useMemo(
185
+ () => createDecorate(rangeDecorationsActor),
186
+ [rangeDecorationsActor],
187
+ )
188
+
189
+ useEffect(() => {
190
+ rangeDecorationsActor.send({
191
+ type: 'update read only',
192
+ readOnly,
193
+ })
194
+ }, [rangeDecorationsActor, readOnly])
195
+
196
+ useEffect(() => {
197
+ rangeDecorationsActor.send({
198
+ type: 'range decorations updated',
199
+ rangeDecorations: rangeDecorations ?? [],
200
+ })
201
+ }, [rangeDecorationsActor, rangeDecorations])
202
+
191
203
  const blockTypeName = schemaTypes.block.name
192
204
 
193
205
  // Output a minimal React editor inside Editable when in readOnly mode.
@@ -312,83 +324,20 @@ export const PortableTextEditable = forwardRef<
312
324
  }
313
325
  }, [blockTypeName, editorActor, propsSelection, slateEditor])
314
326
 
315
- const syncRangeDecorations = useCallback(
316
- (operation?: Operation) => {
317
- if (rangeDecorations && rangeDecorations.length > 0) {
318
- const newSlateRanges: BaseRangeWithDecoration[] = []
319
- rangeDecorations.forEach((rangeDecorationItem) => {
320
- const slateRange = toSlateRange(
321
- rangeDecorationItem.selection,
322
- slateEditor,
323
- )
324
- if (!SlateRange.isRange(slateRange)) {
325
- if (rangeDecorationItem.onMoved) {
326
- rangeDecorationItem.onMoved({
327
- newSelection: null,
328
- rangeDecoration: rangeDecorationItem,
329
- origin: 'local',
330
- })
331
- }
332
- return
333
- }
334
- let newRange: BaseRange | null | undefined
335
- if (operation) {
336
- newRange = moveRangeByOperation(slateRange, operation)
337
- if (
338
- (newRange && newRange !== slateRange) ||
339
- (newRange === null && slateRange)
340
- ) {
341
- const newRangeSelection = newRange
342
- ? slateRangeToSelection({
343
- schema: schemaTypes,
344
- editor: slateEditor,
345
- range: newRange,
346
- })
347
- : null
348
- if (rangeDecorationItem.onMoved) {
349
- rangeDecorationItem.onMoved({
350
- newSelection: newRangeSelection,
351
- rangeDecoration: rangeDecorationItem,
352
- origin: 'local',
353
- })
354
- }
355
- }
356
- }
357
- // If the newRange is null, it means that the range is not valid anymore and should be removed
358
- // If it's undefined, it means that the slateRange is still valid and should be kept
359
- if (newRange !== null) {
360
- newSlateRanges.push({
361
- ...(newRange || slateRange),
362
- rangeDecoration: rangeDecorationItem,
363
- })
364
- }
365
- })
366
- if (newSlateRanges.length > 0) {
367
- setRangeDecorationsState(newSlateRanges)
368
- return
369
- }
370
- }
371
- setRangeDecorationsState((rangeDecorationState) => {
372
- // If there's state then we want to reset
373
- if (rangeDecorationState.length > 0) {
374
- return []
375
- }
376
- // Otherwise we no-op, React will skip a state update if what we return has reference equality to the previous state
377
- return rangeDecorationState
378
- })
379
- },
380
- [rangeDecorations, schemaTypes, slateEditor],
381
- )
382
-
383
327
  // Restore selection from props when the editor has been initialized properly with it's value
384
328
  useEffect(() => {
385
329
  const onReady = editorActor.on('ready', () => {
386
- syncRangeDecorations()
330
+ rangeDecorationsActor.send({
331
+ type: 'ready',
332
+ })
333
+
387
334
  restoreSelectionFromProps()
388
335
  })
336
+
389
337
  const onInvalidValue = editorActor.on('invalid value', () => {
390
338
  setHasInvalidValue(true)
391
339
  })
340
+
392
341
  const onValueChanged = editorActor.on('value changed', () => {
393
342
  setHasInvalidValue(false)
394
343
  })
@@ -398,7 +347,7 @@ export const PortableTextEditable = forwardRef<
398
347
  onInvalidValue.unsubscribe()
399
348
  onValueChanged.unsubscribe()
400
349
  }
401
- }, [editorActor, restoreSelectionFromProps, syncRangeDecorations])
350
+ }, [rangeDecorationsActor, editorActor, restoreSelectionFromProps])
402
351
 
403
352
  // Restore selection from props when it changes
404
353
  useEffect(() => {
@@ -407,32 +356,6 @@ export const PortableTextEditable = forwardRef<
407
356
  }
408
357
  }, [hasInvalidValue, propsSelection, restoreSelectionFromProps])
409
358
 
410
- const [syncedRangeDecorations, setSyncedRangeDecorations] = useState(false)
411
- useEffect(() => {
412
- if (!syncedRangeDecorations) {
413
- // We only want this to run once, on mount
414
- setSyncedRangeDecorations(true)
415
- syncRangeDecorations()
416
- }
417
- }, [syncRangeDecorations, syncedRangeDecorations])
418
-
419
- useEffect(() => {
420
- if (!isEqual(rangeDecorations, rangeDecorationsRef.current)) {
421
- syncRangeDecorations()
422
- }
423
- rangeDecorationsRef.current = rangeDecorations
424
- }, [rangeDecorations, syncRangeDecorations])
425
-
426
- // Sync range decorations after an operation is applied
427
- useEffect(() => {
428
- const teardown = withSyncRangeDecorations({
429
- editorActor,
430
- slateEditor,
431
- syncRangeDecorations,
432
- })
433
- return () => teardown()
434
- }, [editorActor, slateEditor, syncRangeDecorations])
435
-
436
359
  // Handle from props onCopy function
437
360
  const handleCopy = useCallback(
438
361
  (event: ClipboardEvent<HTMLDivElement>): void | ReactEditor => {
@@ -855,55 +778,6 @@ export const PortableTextEditable = forwardRef<
855
778
  }
856
779
  }, [portableTextEditor, scrollSelectionIntoView])
857
780
 
858
- const decorate: (entry: NodeEntry) => BaseRange[] = useCallback(
859
- ([, path]) => {
860
- if (isEqualToEmptyEditor(slateEditor.children, schemaTypes)) {
861
- return [
862
- {
863
- anchor: {
864
- path: [0, 0],
865
- offset: 0,
866
- },
867
- focus: {
868
- path: [0, 0],
869
- offset: 0,
870
- },
871
- placeholder: true,
872
- },
873
- ]
874
- }
875
- // Editor node has a path length of 0 (should never be decorated)
876
- if (path.length === 0) {
877
- return []
878
- }
879
- const result = rangeDecorationState.filter((item) => {
880
- // Special case in order to only return one decoration for collapsed ranges
881
- if (SlateRange.isCollapsed(item)) {
882
- // Collapsed ranges should only be decorated if they are on a block child level (length 2)
883
- if (path.length !== 2) {
884
- return false
885
- }
886
- return (
887
- Path.equals(item.focus.path, path) &&
888
- Path.equals(item.anchor.path, path)
889
- )
890
- }
891
- // Include decorations that either include or intersects with this path
892
- return (
893
- SlateRange.intersection(item, {
894
- anchor: {path, offset: 0},
895
- focus: {path, offset: 0},
896
- }) || SlateRange.includes(item, path)
897
- )
898
- })
899
- if (result.length > 0) {
900
- return result
901
- }
902
- return []
903
- },
904
- [slateEditor, schemaTypes, rangeDecorationState],
905
- )
906
-
907
781
  // Set the forwarded ref to be the Slate editable DOM element
908
782
  // Also set the editable element in a state so that the MutationObserver
909
783
  // is setup when this element is ready.
@@ -118,6 +118,32 @@ describe('RangeDecorations', () => {
118
118
  value={value}
119
119
  />,
120
120
  )
121
+ await waitFor(() => {
122
+ expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
123
+ 1,
124
+ 'updated-with-different',
125
+ ])
126
+ })
127
+
128
+ // Update the range decorations with a new offset again
129
+ rangeDecorations = [
130
+ {
131
+ component: RangeDecorationTestComponent,
132
+ selection: {
133
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
134
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
135
+ },
136
+ },
137
+ ]
138
+ rerender(
139
+ <PortableTextEditorTester
140
+ onChange={onChange}
141
+ rangeDecorations={rangeDecorations}
142
+ ref={editorRef}
143
+ schemaType={schemaType}
144
+ value={value}
145
+ />,
146
+ )
121
147
  await waitFor(() => {
122
148
  expect([rangeDecorationIteration, 'updated-with-different']).toEqual([
123
149
  2,