@portabletext/editor 1.10.2 → 1.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.10.2",
3
+ "version": "1.11.0",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -58,11 +58,11 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@portabletext/toolkit": "^2.0.16",
61
- "@sanity/block-tools": "^3.64.1",
61
+ "@sanity/block-tools": "^3.64.2",
62
62
  "@sanity/diff-match-patch": "^3.1.1",
63
- "@sanity/pkg-utils": "^6.11.11",
64
- "@sanity/schema": "^3.64.1",
65
- "@sanity/types": "^3.64.1",
63
+ "@sanity/pkg-utils": "^6.11.12",
64
+ "@sanity/schema": "^3.64.2",
65
+ "@sanity/types": "^3.64.2",
66
66
  "@testing-library/jest-dom": "^6.6.3",
67
67
  "@testing-library/react": "^16.0.1",
68
68
  "@types/debug": "^4.1.5",
@@ -70,8 +70,8 @@
70
70
  "@types/lodash.startcase": "^4.4.9",
71
71
  "@types/react": "^18.3.12",
72
72
  "@types/react-dom": "^18.3.1",
73
- "@typescript-eslint/eslint-plugin": "^8.14.0",
74
- "@typescript-eslint/parser": "^8.14.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.15.0",
74
+ "@typescript-eslint/parser": "^8.15.0",
75
75
  "@vitejs/plugin-react": "^4.3.3",
76
76
  "@vitest/browser": "^2.1.5",
77
77
  "babel-plugin-react-compiler": "19.0.0-beta-0dec889-20241115",
@@ -90,9 +90,9 @@
90
90
  "@sanity/gherkin-driver": "^0.0.1"
91
91
  },
92
92
  "peerDependencies": {
93
- "@sanity/block-tools": "^3.64.1",
94
- "@sanity/schema": "^3.64.1",
95
- "@sanity/types": "^3.64.1",
93
+ "@sanity/block-tools": "^3.64.2",
94
+ "@sanity/schema": "^3.64.2",
95
+ "@sanity/types": "^3.64.2",
96
96
  "react": "^16.9 || ^17 || ^18",
97
97
  "rxjs": "^7.8.1",
98
98
  "styled-components": "^6.1.13"
@@ -160,7 +160,7 @@ export const PortableTextEditable = forwardRef<
160
160
 
161
161
  const editorActor = useContext(EditorActorContext)
162
162
  const readOnly = useSelector(editorActor, (s) => s.context.readOnly)
163
- const {schemaTypes} = portableTextEditor
163
+ const schemaTypes = useSelector(editorActor, (s) => s.context.schema)
164
164
  const slateEditor = useSlate()
165
165
 
166
166
  const blockTypeName = schemaTypes.block.name
@@ -14,7 +14,7 @@ import {
14
14
  } from 'react'
15
15
  import {Subject} from 'rxjs'
16
16
  import {Slate} from 'slate-react'
17
- import {createActor} from 'xstate'
17
+ import {useEffectEvent} from 'use-effect-event'
18
18
  import type {
19
19
  EditableAPI,
20
20
  EditableAPIDeleteOptions,
@@ -28,17 +28,13 @@ import {debugWithName} from '../utils/debug'
28
28
  import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
29
29
  import {compileType} from '../utils/schema'
30
30
  import {Synchronizer} from './components/Synchronizer'
31
- import {createSlateEditor, type SlateEditor} from './create-slate-editor'
32
31
  import {EditorActorContext} from './editor-actor-context'
33
- import {editorMachine, type EditorActor} from './editor-machine'
32
+ import type {EditorActor} from './editor-machine'
34
33
  import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
35
34
  import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection'
36
35
  import {defaultKeyGenerator} from './key-generator'
37
- import {
38
- createEditableAPI,
39
- type AddedAnnotationPaths,
40
- } from './plugins/createWithEditableAPI'
41
- import type {Editor} from './use-editor'
36
+ import type {AddedAnnotationPaths} from './plugins/createWithEditableAPI'
37
+ import {createEditor, type Editor} from './use-editor'
42
38
 
43
39
  const debug = debugWithName('component:PortableTextEditor')
44
40
 
@@ -124,71 +120,35 @@ export class PortableTextEditor extends Component<
124
120
  */
125
121
  public schemaTypes: PortableTextMemberSchemaTypes
126
122
  /**
123
+ * The editor instance
124
+ */
125
+ private editor: Editor
126
+ /*
127
127
  * The editor API (currently implemented with Slate).
128
128
  */
129
- private editable?: EditableAPI
130
- private editorActor: EditorActor
131
- private slateEditor: SlateEditor
129
+ private editable: EditableAPI
132
130
 
133
131
  constructor(props: PortableTextEditorProps) {
134
132
  super(props)
135
133
 
136
134
  if (props.editor) {
137
- const editor = props.editor as Editor
138
- this.editorActor = editor._internal.editorActor
139
- this.slateEditor = editor._internal.slateEditor
140
- this.editorActor.start()
141
- this.schemaTypes = this.editorActor.getSnapshot().context.schema
135
+ this.editor = props.editor as Editor
142
136
  } else {
143
- if (!props.schemaType) {
144
- throw new Error('PortableTextEditor: missing "schemaType" property')
145
- }
146
-
147
- if (props.incomingPatches$) {
148
- console.warn(
149
- `The prop 'incomingPatches$' is deprecated and renamed to 'patches$'`,
150
- )
151
- }
152
-
153
- this.schemaTypes = getPortableTextMemberSchemaTypes(
154
- props.schemaType.hasOwnProperty('jsonType')
155
- ? props.schemaType
156
- : compileType(props.schemaType),
157
- )
158
-
159
- this.editorActor = createActor(editorMachine, {
160
- input: {
161
- keyGenerator: props.keyGenerator || defaultKeyGenerator,
162
- schema: this.schemaTypes,
163
- value: props.value,
164
- },
165
- })
166
- this.editorActor.start()
167
-
168
- this.slateEditor = createSlateEditor({
169
- editorActor: this.editorActor,
137
+ this.editor = createEditor({
138
+ keyGenerator: props.keyGenerator ?? defaultKeyGenerator,
139
+ schema: props.schemaType,
140
+ initialValue: props.value,
141
+ maxBlocks:
142
+ props.maxBlocks === undefined
143
+ ? undefined
144
+ : Number.parseInt(props.maxBlocks.toString(), 10),
145
+ readOnly: props.readOnly,
170
146
  })
171
-
172
- if (props.readOnly) {
173
- this.editorActor.send({
174
- type: 'toggle readOnly',
175
- })
176
- }
177
-
178
- if (props.maxBlocks) {
179
- this.editorActor.send({
180
- type: 'update maxBlocks',
181
- maxBlocks:
182
- props.maxBlocks === undefined
183
- ? undefined
184
- : Number.parseInt(props.maxBlocks.toString(), 10),
185
- })
186
- }
187
147
  }
188
- this.editable = createEditableAPI(
189
- this.slateEditor.instance,
190
- this.editorActor,
191
- )
148
+
149
+ this.schemaTypes =
150
+ this.editor._internal.editorActor.getSnapshot().context.schema
151
+ this.editable = this.editor.editable
192
152
  }
193
153
 
194
154
  componentDidUpdate(prevProps: PortableTextEditorProps) {
@@ -204,7 +164,7 @@ export class PortableTextEditor extends Component<
204
164
  : compileType(this.props.schemaType),
205
165
  )
206
166
 
207
- this.editorActor.send({
167
+ this.editor._internal.editorActor.send({
208
168
  type: 'update schema',
209
169
  schema: this.schemaTypes,
210
170
  })
@@ -212,13 +172,13 @@ export class PortableTextEditor extends Component<
212
172
 
213
173
  if (!this.props.editor && !prevProps.editor) {
214
174
  if (this.props.readOnly !== prevProps.readOnly) {
215
- this.editorActor.send({
175
+ this.editor._internal.editorActor.send({
216
176
  type: 'toggle readOnly',
217
177
  })
218
178
  }
219
179
 
220
180
  if (this.props.maxBlocks !== prevProps.maxBlocks) {
221
- this.editorActor.send({
181
+ this.editor._internal.editorActor.send({
222
182
  type: 'update maxBlocks',
223
183
  maxBlocks:
224
184
  this.props.maxBlocks === undefined
@@ -228,7 +188,7 @@ export class PortableTextEditor extends Component<
228
188
  }
229
189
 
230
190
  if (this.props.value !== prevProps.value) {
231
- this.editorActor.send({
191
+ this.editor._internal.editorActor.send({
232
192
  type: 'update value',
233
193
  value: this.props.value,
234
194
  })
@@ -244,15 +204,7 @@ export class PortableTextEditor extends Component<
244
204
  }
245
205
 
246
206
  public setEditable = (editable: EditableAPI) => {
247
- this.editable = {...this.editable, ...editable}
248
- }
249
-
250
- private getValue = () => {
251
- if (this.editable) {
252
- return this.editable.getValue()
253
- }
254
-
255
- return undefined
207
+ this.editor.editable = {...this.editor.editable, ...editable}
256
208
  }
257
209
 
258
210
  render() {
@@ -264,33 +216,38 @@ export class PortableTextEditor extends Component<
264
216
  <>
265
217
  {legacyPatches ? (
266
218
  <RoutePatchesObservableToEditorActor
267
- editorActor={this.editorActor}
219
+ editorActor={this.editor._internal.editorActor}
268
220
  patches$={legacyPatches}
269
221
  />
270
222
  ) : null}
271
- <EditorActorContext.Provider value={this.editorActor}>
223
+ <RouteEventsToChanges
224
+ editorActor={this.editor._internal.editorActor}
225
+ onChange={(change) => {
226
+ if (!this.props.editor) {
227
+ this.props.onChange(change)
228
+ }
229
+ /**
230
+ * For backwards compatibility, we relay all changes to the
231
+ * `change$` Subject as well.
232
+ */
233
+ this.change$.next(change)
234
+ }}
235
+ />
236
+ <Synchronizer
237
+ editorActor={this.editor._internal.editorActor}
238
+ getValue={this.editor.editable.getValue}
239
+ portableTextEditor={this}
240
+ slateEditor={this.editor._internal.slateEditor.instance}
241
+ />
242
+ <EditorActorContext.Provider value={this.editor._internal.editorActor}>
272
243
  <Slate
273
- editor={this.slateEditor.instance}
274
- initialValue={this.slateEditor.initialValue}
244
+ editor={this.editor._internal.slateEditor.instance}
245
+ initialValue={this.editor._internal.slateEditor.initialValue}
275
246
  >
276
247
  <PortableTextEditorContext.Provider value={this}>
277
248
  <PortableTextEditorSelectionProvider
278
- editorActor={this.editorActor}
249
+ editorActor={this.editor._internal.editorActor}
279
250
  >
280
- <Synchronizer
281
- editorActor={this.editorActor}
282
- getValue={this.getValue}
283
- onChange={(change) => {
284
- if (!this.props.editor) {
285
- this.props.onChange(change)
286
- }
287
- /**
288
- * For backwards compatibility, we relay all changes to the
289
- * `change$` Subject as well.
290
- */
291
- this.change$.next(change)
292
- }}
293
- />
294
251
  {this.props.children}
295
252
  </PortableTextEditorSelectionProvider>
296
253
  </PortableTextEditorContext.Provider>
@@ -468,3 +425,73 @@ function RoutePatchesObservableToEditorActor(props: {
468
425
 
469
426
  return null
470
427
  }
428
+
429
+ export function RouteEventsToChanges(props: {
430
+ editorActor: EditorActor
431
+ onChange: (change: EditorChange) => void
432
+ }) {
433
+ // We want to ensure that _when_ `props.onChange` is called, it uses the current value.
434
+ // But we don't want to have the `useEffect` run setup + teardown + setup every time the prop might change, as that's unnecessary.
435
+ // So we use our own polyfill that lets us use an upcoming React hook that solves this exact problem.
436
+ // https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event
437
+ const handleChange = useEffectEvent((change: EditorChange) =>
438
+ props.onChange(change),
439
+ )
440
+
441
+ useEffect(() => {
442
+ debug('Subscribing to editor changes')
443
+ const sub = props.editorActor.on('*', (event) => {
444
+ switch (event.type) {
445
+ case 'patch':
446
+ handleChange(event)
447
+ break
448
+ case 'loading': {
449
+ handleChange({type: 'loading', isLoading: true})
450
+ break
451
+ }
452
+ case 'done loading': {
453
+ handleChange({type: 'loading', isLoading: false})
454
+ break
455
+ }
456
+ case 'focused': {
457
+ handleChange({type: 'focus', event: event.event})
458
+ break
459
+ }
460
+ case 'value changed': {
461
+ handleChange({type: 'value', value: event.value})
462
+ break
463
+ }
464
+ case 'invalid value': {
465
+ handleChange({
466
+ type: 'invalidValue',
467
+ resolution: event.resolution,
468
+ value: event.value,
469
+ })
470
+ break
471
+ }
472
+ case 'error': {
473
+ handleChange({
474
+ ...event,
475
+ level: 'warning',
476
+ })
477
+ break
478
+ }
479
+ case 'annotation.add':
480
+ case 'annotation.remove':
481
+ case 'annotation.toggle':
482
+ case 'focus':
483
+ case 'patches':
484
+ case 'readOnly toggled':
485
+ break
486
+ default:
487
+ handleChange(event)
488
+ }
489
+ })
490
+ return () => {
491
+ debug('Unsubscribing to changes')
492
+ sub.unsubscribe()
493
+ }
494
+ }, [props.editorActor, handleChange])
495
+
496
+ return null
497
+ }
@@ -212,3 +212,12 @@ export type PickFromUnion<
212
212
  TTagKey extends keyof TUnion,
213
213
  TPickedTags extends TUnion[TTagKey],
214
214
  > = TUnion extends Record<TTagKey, TPickedTags> ? TUnion : never
215
+
216
+ /**
217
+ * @alpha
218
+ */
219
+ export type OmitFromUnion<
220
+ TUnion,
221
+ TTagKey extends keyof TUnion,
222
+ TOmittedTags extends TUnion[TTagKey],
223
+ > = TUnion extends Record<TTagKey, TOmittedTags> ? never : TUnion
@@ -4,14 +4,12 @@ import {useSelector} from '@xstate/react'
4
4
  import {throttle} from 'lodash'
5
5
  import {useCallback, useEffect, useRef} from 'react'
6
6
  import {Editor} from 'slate'
7
- import {useSlate} from 'slate-react'
8
- import {useEffectEvent} from 'use-effect-event'
9
- import type {EditorChange} from '../../types/editor'
7
+ import type {PortableTextSlateEditor} from '../../types/editor'
10
8
  import {debugWithName} from '../../utils/debug'
11
9
  import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
12
10
  import type {EditorActor} from '../editor-machine'
13
- import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
14
11
  import {useSyncValue} from '../hooks/useSyncValue'
12
+ import type {PortableTextEditor} from '../PortableTextEditor'
15
13
 
16
14
  const debug = debugWithName('component:PortableTextEditor:Synchronizer')
17
15
  const debugVerbose = debug.enabled && false
@@ -26,7 +24,8 @@ const FLUSH_PATCHES_THROTTLED_MS = process.env.NODE_ENV === 'test' ? 500 : 1000
26
24
  export interface SynchronizerProps {
27
25
  editorActor: EditorActor
28
26
  getValue: () => Array<PortableTextBlock> | undefined
29
- onChange: (change: EditorChange) => void
27
+ portableTextEditor: PortableTextEditor
28
+ slateEditor: PortableTextSlateEditor
30
29
  }
31
30
 
32
31
  /**
@@ -34,20 +33,18 @@ export interface SynchronizerProps {
34
33
  * @internal
35
34
  */
36
35
  export function Synchronizer(props: SynchronizerProps) {
37
- const portableTextEditor = usePortableTextEditor()
38
36
  const readOnly = useSelector(props.editorActor, (s) => s.context.readOnly)
39
37
  const value = useSelector(props.editorActor, (s) => s.context.value)
40
- const {editorActor, getValue, onChange} = props
38
+ const {editorActor, getValue, portableTextEditor, slateEditor} = props
41
39
  const pendingPatches = useRef<Patch[]>([])
42
40
 
43
41
  const syncValue = useSyncValue({
44
42
  editorActor,
45
43
  portableTextEditor,
46
44
  readOnly,
45
+ slateEditor,
47
46
  })
48
47
 
49
- const slateEditor = useSlate()
50
-
51
48
  useEffect(() => {
52
49
  IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
53
50
  }, [slateEditor])
@@ -76,14 +73,6 @@ export function Synchronizer(props: SynchronizerProps) {
76
73
  }
77
74
  }, [onFlushPendingPatches])
78
75
 
79
- // We want to ensure that _when_ `props.onChange` is called, it uses the current value.
80
- // But we don't want to have the `useEffect` run setup + teardown + setup every time the prop might change, as that's unnecessary.
81
- // So we use our own polyfill that lets us use an upcoming React hook that solves this exact problem.
82
- // https://19.react.dev/learn/separating-events-from-effects#declaring-an-effect-event
83
- const handleChange = useEffectEvent((change: EditorChange) =>
84
- onChange(change),
85
- )
86
-
87
76
  // Subscribe to, and handle changes from the editor
88
77
  useEffect(() => {
89
78
  const onFlushPendingPatchesThrottled = throttle(
@@ -104,61 +93,17 @@ export function Synchronizer(props: SynchronizerProps) {
104
93
  },
105
94
  )
106
95
 
107
- debug('Subscribing to editor changes')
108
- const sub = editorActor.on('*', (event) => {
109
- switch (event.type) {
110
- case 'patch':
111
- IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
112
- pendingPatches.current.push(event.patch)
113
- onFlushPendingPatchesThrottled()
114
- handleChange(event)
115
- break
116
- case 'loading': {
117
- handleChange({type: 'loading', isLoading: true})
118
- break
119
- }
120
- case 'done loading': {
121
- handleChange({type: 'loading', isLoading: false})
122
- break
123
- }
124
- case 'focused': {
125
- handleChange({type: 'focus', event: event.event})
126
- break
127
- }
128
- case 'value changed': {
129
- handleChange({type: 'value', value: event.value})
130
- break
131
- }
132
- case 'invalid value': {
133
- handleChange({
134
- type: 'invalidValue',
135
- resolution: event.resolution,
136
- value: event.value,
137
- })
138
- break
139
- }
140
- case 'error': {
141
- handleChange({
142
- ...event,
143
- level: 'warning',
144
- })
145
- break
146
- }
147
- case 'annotation.add':
148
- case 'annotation.remove':
149
- case 'annotation.toggle':
150
- case 'focus':
151
- case 'patches':
152
- break
153
- default:
154
- handleChange(event)
155
- }
96
+ debug('Subscribing to patch events')
97
+ const sub = editorActor.on('patch', (event) => {
98
+ IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, true)
99
+ pendingPatches.current.push(event.patch)
100
+ onFlushPendingPatchesThrottled()
156
101
  })
157
102
  return () => {
158
- debug('Unsubscribing to changes')
103
+ debug('Unsubscribing to patch events')
159
104
  sub.unsubscribe()
160
105
  }
161
- }, [editorActor, handleChange, onFlushPendingPatches, slateEditor])
106
+ }, [editorActor, onFlushPendingPatches, slateEditor])
162
107
 
163
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.
164
109
  // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
@@ -32,8 +32,8 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
32
32
 
33
33
  debug('Creating new Slate editor instance', config.editorActor.id)
34
34
 
35
- let unsubscriptions: Array<() => void> = []
36
- let subscriptions: Array<() => () => void> = []
35
+ const unsubscriptions: Array<() => void> = []
36
+ const subscriptions: Array<() => () => void> = []
37
37
 
38
38
  const instance = withPlugins(withReact(createEditor()), {
39
39
  editorActor: config.editorActor,
@@ -47,18 +47,6 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
47
47
  unsubscriptions.push(subscription())
48
48
  }
49
49
 
50
- config.editorActor.subscribe((snapshot) => {
51
- if (snapshot.status !== 'active') {
52
- debug('Destroying Slate editor')
53
- instance.destroy()
54
- for (const unsubscribe of unsubscriptions) {
55
- unsubscribe()
56
- }
57
- subscriptions = []
58
- unsubscriptions = []
59
- }
60
- })
61
-
62
50
  const initialValue = [instance.pteCreateTextBlock({decorators: []})]
63
51
 
64
52
  const slateEditor: SlateEditor = {
@@ -0,0 +1,24 @@
1
+ import {useEffect} from 'react'
2
+ import {useEffectEvent} from 'use-effect-event'
3
+ import type {EditorEmittedEvent} from './editor-machine'
4
+ import {useEditorContext} from './editor-provider'
5
+
6
+ /**
7
+ * @alpha
8
+ */
9
+ export function EditorEventListener(props: {
10
+ on: (event: EditorEmittedEvent) => void
11
+ }) {
12
+ const editor = useEditorContext()
13
+ const on = useEffectEvent(props.on)
14
+
15
+ useEffect(() => {
16
+ const subscription = editor.on('*', on)
17
+
18
+ return () => {
19
+ subscription.unsubscribe()
20
+ }
21
+ }, [editor, on])
22
+
23
+ return null
24
+ }
@@ -27,6 +27,7 @@ import type {
27
27
  BehaviorActionIntend,
28
28
  BehaviorContext,
29
29
  BehaviorEvent,
30
+ OmitFromUnion,
30
31
  PickFromUnion,
31
32
  } from './behavior/behavior.types'
32
33
 
@@ -95,7 +96,27 @@ export type InternalEditorEvent =
95
96
  type: 'update maxBlocks'
96
97
  maxBlocks: number | undefined
97
98
  }
98
- | InternalEditorEmittedEvent
99
+ | OmitFromUnion<InternalEditorEmittedEvent, 'type', 'readOnly toggled'>
100
+
101
+ /**
102
+ * @alpha
103
+ */
104
+ export type EditorEmittedEvent = PickFromUnion<
105
+ InternalEditorEmittedEvent,
106
+ 'type',
107
+ | 'blur'
108
+ | 'done loading'
109
+ | 'error'
110
+ | 'focus'
111
+ | 'invalid value'
112
+ | 'loading'
113
+ | 'mutation'
114
+ | 'patch'
115
+ | 'readOnly toggled'
116
+ | 'ready'
117
+ | 'selection'
118
+ | 'value changed'
119
+ >
99
120
 
100
121
  /**
101
122
  * @internal
@@ -129,6 +150,7 @@ export type InternalEditorEmittedEvent =
129
150
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
130
151
  | {type: 'loading'}
131
152
  | {type: 'done loading'}
153
+ | {type: 'readOnly toggled'; readOnly: boolean}
132
154
  | PickFromUnion<
133
155
  BehaviorEvent,
134
156
  'type',
@@ -154,6 +176,8 @@ export const editorMachine = setup({
154
176
  input: {} as {
155
177
  behaviors?: Array<Behavior>
156
178
  keyGenerator: () => string
179
+ maxBlocks?: number
180
+ readOnly?: boolean
157
181
  schema: PortableTextMemberSchemaTypes
158
182
  value?: Array<PortableTextBlock>
159
183
  },
@@ -296,8 +320,8 @@ export const editorMachine = setup({
296
320
  keyGenerator: input.keyGenerator,
297
321
  pendingEvents: [],
298
322
  schema: input.schema,
299
- readOnly: false,
300
- maxBlocks: undefined,
323
+ readOnly: input.readOnly ?? false,
324
+ maxBlocks: input.maxBlocks,
301
325
  value: input.value,
302
326
  }),
303
327
  on: {
@@ -332,7 +356,13 @@ export const editorMachine = setup({
332
356
  'update schema': {actions: 'assign schema'},
333
357
  'update value': {actions: assign({value: ({event}) => event.value})},
334
358
  'toggle readOnly': {
335
- actions: assign({readOnly: ({context}) => !context.readOnly}),
359
+ actions: [
360
+ assign({readOnly: ({context}) => !context.readOnly}),
361
+ emit(({context}) => ({
362
+ type: 'readOnly toggled',
363
+ readOnly: context.readOnly,
364
+ })),
365
+ ],
336
366
  },
337
367
  'update maxBlocks': {
338
368
  actions: assign({maxBlocks: ({event}) => event.maxBlocks}),