@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
@@ -25,10 +25,10 @@ import type {
25
25
  PortableTextMemberSchemaTypes,
26
26
  } from '../types/editor'
27
27
  import {debugWithName} from '../utils/debug'
28
- import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
29
28
  import {compileType} from '../utils/schema'
30
29
  import {Synchronizer} from './components/Synchronizer'
31
30
  import {createEditor, type Editor} from './create-editor'
31
+ import {createEditorSchema} from './create-editor-schema'
32
32
  import {EditorActorContext} from './editor-actor-context'
33
33
  import type {EditorActor} from './editor-machine'
34
34
  import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
@@ -158,7 +158,7 @@ export class PortableTextEditor extends Component<
158
158
  !prevProps.editor &&
159
159
  this.props.schemaType !== prevProps.schemaType
160
160
  ) {
161
- this.schemaTypes = getPortableTextMemberSchemaTypes(
161
+ this.schemaTypes = createEditorSchema(
162
162
  this.props.schemaType.hasOwnProperty('jsonType')
163
163
  ? this.props.schemaType
164
164
  : compileType(this.props.schemaType),
@@ -238,8 +238,6 @@ export class PortableTextEditor extends Component<
238
238
  />
239
239
  <Synchronizer
240
240
  editorActor={this.editor._internal.editorActor}
241
- getValue={this.editor._internal.editable.getValue}
242
- portableTextEditor={this}
243
241
  slateEditor={this.editor._internal.slateEditor.instance}
244
242
  />
245
243
  <EditorActorContext.Provider value={this.editor._internal.editorActor}>
@@ -496,7 +494,8 @@ export function RouteEventsToChanges(props: {
496
494
  case 'list item.toggle':
497
495
  case 'style.toggle':
498
496
  case 'patches':
499
- case 'readOnly toggled':
497
+ case 'editable':
498
+ case 'read only':
500
499
  break
501
500
  default:
502
501
  handleChange(event)
@@ -1,11 +1,8 @@
1
1
  import {render, waitFor} from '@testing-library/react'
2
2
  import {createRef, type RefObject} from 'react'
3
3
  import {describe, expect, it, vi} from 'vitest'
4
- import {
5
- PortableTextEditorTester,
6
- schemaType,
7
- } from '../__tests__/PortableTextEditorTester'
8
4
  import {PortableTextEditor} from '../PortableTextEditor'
5
+ import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
9
6
 
10
7
  const initialValue = [
11
8
  {
@@ -52,6 +49,17 @@ describe('useSyncValue', () => {
52
49
  value={initialValue}
53
50
  />,
54
51
  )
52
+
53
+ await waitFor(() => {
54
+ if (editorRef.current) {
55
+ expect(onChange).toHaveBeenCalledWith({
56
+ type: 'value',
57
+ value: initialValue,
58
+ })
59
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
60
+ }
61
+ })
62
+
55
63
  rerender(
56
64
  <PortableTextEditorTester
57
65
  onChange={onChange}
@@ -60,6 +68,7 @@ describe('useSyncValue', () => {
60
68
  value={syncedValue}
61
69
  />,
62
70
  )
71
+
63
72
  await waitFor(() => {
64
73
  if (editorRef.current) {
65
74
  expect(PortableTextEditor.getValue(editorRef.current)).toEqual(
@@ -68,6 +77,7 @@ describe('useSyncValue', () => {
68
77
  }
69
78
  })
70
79
  })
80
+
71
81
  it('replaces span nodes with different keys inside the same children array', async () => {
72
82
  const editorRef: RefObject<PortableTextEditor | null> = createRef()
73
83
  const onChange = vi.fn()
@@ -95,6 +105,17 @@ describe('useSyncValue', () => {
95
105
  value={initialValue}
96
106
  />,
97
107
  )
108
+
109
+ await waitFor(() => {
110
+ if (editorRef.current) {
111
+ expect(onChange).toHaveBeenCalledWith({
112
+ type: 'value',
113
+ value: initialValue,
114
+ })
115
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
116
+ }
117
+ })
118
+
98
119
  rerender(
99
120
  <PortableTextEditorTester
100
121
  onChange={onChange}
@@ -103,27 +124,25 @@ describe('useSyncValue', () => {
103
124
  value={syncedValue}
104
125
  />,
105
126
  )
127
+
106
128
  await waitFor(() => {
107
129
  if (editorRef.current) {
108
- expect(PortableTextEditor.getValue(editorRef.current))
109
- .toMatchInlineSnapshot(`
110
- [
111
- {
112
- "_key": "77071c3af231",
113
- "_type": "myTestBlockType",
114
- "children": [
115
- {
116
- "_key": "c001f0e92c1f0__NEW_KEY_YA!",
117
- "_type": "span",
118
- "marks": [],
119
- "text": "Lorem ipsum dolor sit amet, consectetur adipiscing elit. ",
120
- },
121
- ],
122
- "markDefs": [],
123
- "style": "normal",
124
- },
125
- ]
126
- `)
130
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
131
+ {
132
+ _key: '77071c3af231',
133
+ _type: 'myTestBlockType',
134
+ children: [
135
+ {
136
+ _key: 'c001f0e92c1f0__NEW_KEY_YA!',
137
+ _type: 'span',
138
+ marks: [],
139
+ text: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. ',
140
+ },
141
+ ],
142
+ markDefs: [],
143
+ style: 'normal',
144
+ },
145
+ ])
127
146
  }
128
147
  })
129
148
  })
@@ -1,30 +1,18 @@
1
- import type {Patch} from '@portabletext/patches'
2
- import type {PortableTextBlock} from '@sanity/types'
3
- import {useSelector} from '@xstate/react'
4
- import {throttle} from 'lodash'
5
- import {useCallback, useEffect, useRef} from 'react'
6
- import {Editor} from 'slate'
1
+ import {useActorRef, useSelector} from '@xstate/react'
2
+ import {useEffect} from 'react'
7
3
  import type {PortableTextSlateEditor} from '../../types/editor'
8
4
  import {debugWithName} from '../../utils/debug'
9
- import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
10
5
  import type {EditorActor} from '../editor-machine'
11
- import {useSyncValue} from '../hooks/useSyncValue'
12
- import type {PortableTextEditor} from '../PortableTextEditor'
6
+ import {mutationMachine} from '../mutation-machine'
7
+ import {syncMachine} from '../sync-machine'
13
8
 
14
9
  const debug = debugWithName('component:PortableTextEditor:Synchronizer')
15
- const debugVerbose = debug.enabled && false
16
-
17
- // The editor will commit changes in a throttled fashion in order
18
- // not to overload the network and degrade performance while typing.
19
- const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
20
10
 
21
11
  /**
22
12
  * @internal
23
13
  */
24
14
  export interface SynchronizerProps {
25
15
  editorActor: EditorActor
26
- getValue: () => Array<PortableTextBlock> | undefined
27
- portableTextEditor: PortableTextEditor
28
16
  slateEditor: PortableTextSlateEditor
29
17
  }
30
18
 
@@ -33,90 +21,75 @@ export interface SynchronizerProps {
33
21
  * @internal
34
22
  */
35
23
  export function Synchronizer(props: SynchronizerProps) {
36
- const readOnly = useSelector(props.editorActor, (s) => s.context.readOnly)
37
- const value = useSelector(props.editorActor, (s) => s.context.value)
38
- const {editorActor, getValue, portableTextEditor, slateEditor} = props
39
- const pendingPatches = useRef<Patch[]>([])
24
+ const {editorActor, slateEditor} = props
40
25
 
41
- const syncValue = useSyncValue({
42
- editorActor,
43
- portableTextEditor,
44
- readOnly,
45
- slateEditor,
26
+ const value = useSelector(props.editorActor, (s) => s.context.value)
27
+ const readOnly = useSelector(props.editorActor, (s) =>
28
+ s.matches({'edit mode': 'read only'}),
29
+ )
30
+ const syncActorRef = useActorRef(syncMachine, {
31
+ input: {
32
+ keyGenerator: props.editorActor.getSnapshot().context.keyGenerator,
33
+ readOnly: props.editorActor
34
+ .getSnapshot()
35
+ .matches({'edit mode': 'read only'}),
36
+ schema: props.editorActor.getSnapshot().context.schema,
37
+ slateEditor,
38
+ },
39
+ })
40
+ const mutationActorRef = useActorRef(mutationMachine, {
41
+ input: {
42
+ schema: editorActor.getSnapshot().context.schema,
43
+ slateEditor,
44
+ },
46
45
  })
47
46
 
48
47
  useEffect(() => {
49
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
50
- }, [slateEditor])
51
-
52
- const onFlushPendingPatches = useCallback(() => {
53
- if (pendingPatches.current.length > 0) {
54
- debug('Flushing pending patches')
55
- if (debugVerbose) {
56
- debug(`Patches:\n${JSON.stringify(pendingPatches.current, null, 2)}`)
48
+ const subscription = mutationActorRef.on('*', (event) => {
49
+ if (event.type === 'has pending patches') {
50
+ syncActorRef.send({type: 'has pending patches'})
51
+ }
52
+ if (event.type === 'mutation') {
53
+ syncActorRef.send({type: 'mutation'})
54
+ editorActor.send(event)
57
55
  }
58
- const snapshot = getValue()
59
- editorActor.send({
60
- type: 'mutation',
61
- patches: pendingPatches.current,
62
- snapshot,
63
- })
64
- pendingPatches.current = []
56
+ })
57
+
58
+ return () => {
59
+ subscription.unsubscribe()
65
60
  }
66
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
67
- }, [editorActor, slateEditor, getValue])
61
+ }, [mutationActorRef, syncActorRef, editorActor])
68
62
 
69
- // Flush pending patches immediately on unmount
70
63
  useEffect(() => {
64
+ const subscription = syncActorRef.on('*', (event) => {
65
+ props.editorActor.send(event)
66
+ })
67
+
71
68
  return () => {
72
- onFlushPendingPatches()
69
+ subscription.unsubscribe()
73
70
  }
74
- }, [onFlushPendingPatches])
71
+ }, [props.editorActor, syncActorRef])
75
72
 
76
- // Subscribe to, and handle changes from the editor
77
73
  useEffect(() => {
78
- const onFlushPendingPatchesThrottled = throttle(
79
- () => {
80
- // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform,
81
- // and we can flush these changes immediately.
82
- if (Editor.isNormalizing(slateEditor)) {
83
- onFlushPendingPatches()
84
- return
85
- }
86
- // If it's in the middle of something, try again.
87
- onFlushPendingPatchesThrottled()
88
- },
89
- FLUSH_PATCHES_THROTTLED_MS,
90
- {
91
- leading: false,
92
- trailing: true,
93
- },
94
- )
74
+ syncActorRef.send({type: 'toggle readOnly'})
75
+ }, [syncActorRef, readOnly])
95
76
 
77
+ useEffect(() => {
78
+ debug('Value from props changed, syncing new value')
79
+ syncActorRef.send({type: 'update value', value})
80
+ }, [syncActorRef, value])
81
+
82
+ // Subscribe to, and handle changes from the editor
83
+ useEffect(() => {
96
84
  debug('Subscribing to patch events')
97
85
  const sub = editorActor.on('patch', (event) => {
98
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
99
- pendingPatches.current.push(event.patch)
100
- onFlushPendingPatchesThrottled()
86
+ mutationActorRef.send(event)
101
87
  })
102
88
  return () => {
103
89
  debug('Unsubscribing to patch events')
104
90
  sub.unsubscribe()
105
91
  }
106
- }, [editorActor, onFlushPendingPatches, slateEditor])
107
-
108
- // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook.
109
- // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
110
- const isInitialValueFromProps = useRef(true)
111
- useEffect(() => {
112
- debug('Value from props changed, syncing new value')
113
- syncValue(value)
114
- // Signal that we have our first value, and are ready to roll.
115
- if (isInitialValueFromProps.current) {
116
- editorActor.send({type: 'ready'})
117
- isInitialValueFromProps.current = false
118
- }
119
- }, [editorActor, syncValue, value])
92
+ }, [editorActor, mutationActorRef, slateEditor])
120
93
 
121
94
  return null
122
95
  }
@@ -6,11 +6,11 @@ import type {
6
6
  SchemaType,
7
7
  SpanSchemaType,
8
8
  } from '@sanity/types'
9
- import type {PortableTextMemberSchemaTypes} from '../types/editor'
9
+ import type {EditorSchema} from './define-schema'
10
10
 
11
- export function getPortableTextMemberSchemaTypes(
11
+ export function createEditorSchema(
12
12
  portableTextType: ArraySchemaType<PortableTextBlock>,
13
- ): PortableTextMemberSchemaTypes {
13
+ ): EditorSchema {
14
14
  if (!portableTextType) {
15
15
  throw new Error("Parameter 'portabletextType' missing (required)")
16
16
  }
@@ -14,8 +14,8 @@ import {
14
14
  import type {Behavior} from '../behaviors/behavior.types'
15
15
  import type {PickFromUnion} from '../type-utils'
16
16
  import type {EditableAPI} from '../types/editor'
17
- import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
18
17
  import {compileType} from '../utils/schema'
18
+ import {createEditorSchema} from './create-editor-schema'
19
19
  import {createSlateEditor, type SlateEditor} from './create-slate-editor'
20
20
  import {compileSchemaDefinition, type SchemaDefinition} from './define-schema'
21
21
  import {
@@ -107,7 +107,7 @@ function editorConfigToMachineInput(config: EditorConfig) {
107
107
  readOnly: config.readOnly,
108
108
  schema: config.schemaDefinition
109
109
  ? compileSchemaDefinition(config.schemaDefinition)
110
- : getPortableTextMemberSchemaTypes(
110
+ : createEditorSchema(
111
111
  config.schema.hasOwnProperty('jsonType')
112
112
  ? config.schema
113
113
  : compileType(config.schema),
@@ -7,7 +7,7 @@ import {
7
7
  } from '@sanity/types'
8
8
  import startCase from 'lodash.startcase'
9
9
  import type {PortableTextMemberSchemaTypes} from '../types/editor'
10
- import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
10
+ import {createEditorSchema} from './create-editor-schema'
11
11
 
12
12
  /**
13
13
  * @alpha
@@ -41,6 +41,11 @@ export function defineSchema<const TSchemaDefinition extends SchemaDefinition>(
41
41
  return definition
42
42
  }
43
43
 
44
+ /**
45
+ * @alpha
46
+ */
47
+ export type EditorSchema = PortableTextMemberSchemaTypes
48
+
44
49
  export function compileSchemaDefinition<
45
50
  TSchemaDefinition extends SchemaDefinition,
46
51
  >(definition?: TSchemaDefinition) {
@@ -111,7 +116,7 @@ export function compileSchemaDefinition<
111
116
  types: [portableTextSchema, ...blockObjects, ...inlineObjects],
112
117
  }).get('portable-text')
113
118
 
114
- const pteSchema = getPortableTextMemberSchemaTypes(schema)
119
+ const pteSchema = createEditorSchema(schema)
115
120
 
116
121
  return {
117
122
  ...pteSchema,
@@ -127,5 +132,5 @@ export function compileSchemaDefinition<
127
132
  } as ObjectSchemaType)
128
133
  : blockObject,
129
134
  ),
130
- } satisfies PortableTextMemberSchemaTypes
135
+ } satisfies EditorSchema
131
136
  }