@portabletext/editor 1.6.0 → 1.7.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 (37) hide show
  1. package/README.md +5 -0
  2. package/lib/index.d.mts +3317 -3488
  3. package/lib/index.d.ts +3317 -3488
  4. package/lib/index.esm.js +4337 -4089
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +4325 -4077
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +4337 -4089
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +10 -11
  11. package/src/editor/Editable.tsx +26 -28
  12. package/src/editor/PortableTextEditor.tsx +90 -66
  13. package/src/editor/behavior/behavior.action.insert-break.ts +12 -2
  14. package/src/editor/behavior/behavior.actions.ts +51 -11
  15. package/src/editor/behavior/behavior.types.ts +23 -0
  16. package/src/editor/components/Synchronizer.tsx +11 -4
  17. package/src/editor/create-slate-editor.tsx +67 -0
  18. package/src/editor/editor-machine.ts +58 -8
  19. package/src/editor/key-generator.ts +30 -1
  20. package/src/editor/plugins/create-with-event-listeners.ts +62 -1
  21. package/src/editor/plugins/createWithEditableAPI.ts +800 -728
  22. package/src/editor/plugins/createWithMaxBlocks.ts +7 -2
  23. package/src/editor/plugins/createWithPatches.ts +4 -4
  24. package/src/editor/plugins/createWithPlaceholderBlock.ts +8 -3
  25. package/src/editor/plugins/createWithPortableTextMarkModel.ts +3 -4
  26. package/src/editor/plugins/createWithUndoRedo.ts +6 -7
  27. package/src/editor/plugins/createWithUtils.ts +2 -8
  28. package/src/editor/plugins/{index.ts → with-plugins.ts} +22 -79
  29. package/src/editor/use-editor.ts +46 -14
  30. package/src/editor/withSyncRangeDecorations.ts +20 -0
  31. package/src/index.ts +9 -1
  32. package/src/types/editor.ts +0 -1
  33. package/src/types/options.ts +1 -3
  34. package/src/utils/__tests__/operationToPatches.test.ts +7 -14
  35. package/src/utils/__tests__/patchToOperations.test.ts +4 -7
  36. package/src/editor/components/SlateContainer.tsx +0 -79
  37. package/src/editor/hooks/usePortableTextReadOnly.ts +0 -20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.6.0",
3
+ "version": "1.7.0",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -45,10 +45,11 @@
45
45
  "@portabletext/patches": "1.1.0",
46
46
  "@xstate/react": "^4.1.3",
47
47
  "debug": "^4.3.4",
48
+ "get-random-values-esm": "^1.0.2",
48
49
  "is-hotkey-esm": "^1.0.0",
49
50
  "lodash": "^4.17.21",
50
51
  "lodash.startcase": "^4.4.0",
51
- "react-compiler-runtime": "19.0.0-beta-63b359f-20241101",
52
+ "react-compiler-runtime": "19.0.0-beta-a7bf2bd-20241110",
52
53
  "slate": "0.110.2",
53
54
  "slate-dom": "^0.111.0",
54
55
  "slate-react": "0.111.0",
@@ -59,10 +60,9 @@
59
60
  "@portabletext/toolkit": "^2.0.16",
60
61
  "@sanity/block-tools": "^3.63.0",
61
62
  "@sanity/diff-match-patch": "^3.1.1",
62
- "@sanity/pkg-utils": "^6.11.9",
63
+ "@sanity/pkg-utils": "^6.11.10",
63
64
  "@sanity/schema": "^3.63.0",
64
65
  "@sanity/types": "^3.63.0",
65
- "@sanity/util": "^3.63.0",
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,13 +70,13 @@
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.13.0",
74
- "@typescript-eslint/parser": "^8.13.0",
73
+ "@typescript-eslint/eslint-plugin": "^8.14.0",
74
+ "@typescript-eslint/parser": "^8.14.0",
75
75
  "@vitejs/plugin-react": "^4.3.3",
76
76
  "@vitest/browser": "^2.1.4",
77
- "babel-plugin-react-compiler": "19.0.0-beta-63b359f-20241101",
77
+ "babel-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
78
78
  "eslint": "8.57.1",
79
- "eslint-plugin-react-compiler": "19.0.0-beta-63b359f-20241101",
79
+ "eslint-plugin-react-compiler": "19.0.0-beta-a7bf2bd-20241110",
80
80
  "eslint-plugin-react-hooks": "^5.0.0",
81
81
  "jsdom": "^25.0.1",
82
82
  "react": "^18.3.1",
@@ -84,7 +84,7 @@
84
84
  "rxjs": "^7.8.1",
85
85
  "styled-components": "^6.1.13",
86
86
  "typescript": "5.6.3",
87
- "vite": "^5.4.10",
87
+ "vite": "^5.4.11",
88
88
  "vitest": "^2.1.4",
89
89
  "vitest-browser-react": "^0.0.3",
90
90
  "@sanity/gherkin-driver": "^0.0.1"
@@ -93,7 +93,6 @@
93
93
  "@sanity/block-tools": "^3.63.0",
94
94
  "@sanity/schema": "^3.63.0",
95
95
  "@sanity/types": "^3.63.0",
96
- "@sanity/util": "^3.63.0",
97
96
  "react": "^16.9 || ^17 || ^18",
98
97
  "rxjs": "^7.8.1",
99
98
  "styled-components": "^6.1.13"
@@ -108,7 +107,7 @@
108
107
  "build": "pkg-utils build --strict --check --clean",
109
108
  "check:lint": "biome lint .",
110
109
  "check:types": "tsc",
111
- "check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --plugin react-hooks --rule 'react-compiler/react-compiler: [warn]' --rule 'react-hooks/rules-of-hooks: [error]' --rule 'react-hooks/exhaustive-deps: [error]' src",
110
+ "check:react-compiler": "eslint --cache --no-inline-config --no-eslintrc --ignore-pattern '**/__tests__/**' --ext .cjs,.mjs,.js,.jsx,.ts,.tsx --parser @typescript-eslint/parser --plugin react-compiler --plugin react-hooks --rule 'react-compiler/react-compiler: [warn,{__unstable_donotuse_reportAllBailouts: true}]' --rule 'react-hooks/rules-of-hooks: [error]' --rule 'react-hooks/exhaustive-deps: [error]' src",
112
111
  "clean": "del .turbo && del lib && del node_modules",
113
112
  "dev": "pkg-utils watch",
114
113
  "lint:fix": "biome lint --write .",
@@ -1,4 +1,5 @@
1
1
  import type {PortableTextBlock} from '@sanity/types'
2
+ import {useSelector} from '@xstate/react'
2
3
  import {isEqual, noop} from 'lodash'
3
4
  import {
4
5
  forwardRef,
@@ -66,9 +67,10 @@ import {Element} from './components/Element'
66
67
  import {Leaf} from './components/Leaf'
67
68
  import {EditorActorContext} from './editor-actor-context'
68
69
  import {usePortableTextEditor} from './hooks/usePortableTextEditor'
69
- import {usePortableTextEditorReadOnlyStatus} from './hooks/usePortableTextReadOnly'
70
- import {createWithHotkeys, createWithInsertData} from './plugins'
70
+ import {createWithHotkeys} from './plugins/createWithHotKeys'
71
+ import {createWithInsertData} from './plugins/createWithInsertData'
71
72
  import {PortableTextEditor} from './PortableTextEditor'
73
+ import {withSyncRangeDecorations} from './withSyncRangeDecorations'
72
74
 
73
75
  const debug = debugWithName('component:Editable')
74
76
 
@@ -139,7 +141,6 @@ export const PortableTextEditable = forwardRef<
139
141
  } = props
140
142
 
141
143
  const portableTextEditor = usePortableTextEditor()
142
- const readOnly = usePortableTextEditorReadOnlyStatus()
143
144
  const ref = useRef<HTMLDivElement | null>(null)
144
145
  const [editableElement, setEditableElement] = useState<HTMLDivElement | null>(
145
146
  null,
@@ -158,32 +159,39 @@ export const PortableTextEditable = forwardRef<
158
159
  const rangeDecorationsRef = useRef(rangeDecorations)
159
160
 
160
161
  const editorActor = useContext(EditorActorContext)
162
+ const readOnly = useSelector(editorActor, (s) => s.context.readOnly)
161
163
  const {schemaTypes} = portableTextEditor
162
164
  const slateEditor = useSlate()
163
165
 
164
166
  const blockTypeName = schemaTypes.block.name
165
167
 
166
- // React/UI-specific plugins
167
- const withInsertData = useMemo(
168
- () => createWithInsertData(editorActor, schemaTypes),
169
- [editorActor, schemaTypes],
170
- )
171
- const withHotKeys = useMemo(
172
- () => createWithHotkeys(editorActor, portableTextEditor, hotkeys),
173
- [editorActor, hotkeys, portableTextEditor],
174
- )
175
-
176
168
  // Output a minimal React editor inside Editable when in readOnly mode.
177
169
  // NOTE: make sure all the plugins used here can be safely run over again at any point.
178
170
  // There will be a problem if they redefine editor methods and then calling the original method within themselves.
179
171
  useMemo(() => {
172
+ // React/UI-specific plugins
173
+ const withInsertData = createWithInsertData(editorActor, schemaTypes)
174
+
180
175
  if (readOnly) {
181
176
  debug('Editable is in read only mode')
182
177
  return withInsertData(slateEditor)
183
178
  }
179
+ const withHotKeys = createWithHotkeys(
180
+ editorActor,
181
+ portableTextEditor,
182
+ hotkeys,
183
+ )
184
+
184
185
  debug('Editable is in edit mode')
185
186
  return withInsertData(withHotKeys(slateEditor))
186
- }, [readOnly, slateEditor, withHotKeys, withInsertData])
187
+ }, [
188
+ editorActor,
189
+ hotkeys,
190
+ portableTextEditor,
191
+ readOnly,
192
+ schemaTypes,
193
+ slateEditor,
194
+ ])
187
195
 
188
196
  const renderElement = useCallback(
189
197
  (eProps: RenderElementProps) => (
@@ -381,9 +389,6 @@ export const PortableTextEditable = forwardRef<
381
389
  }
382
390
  }, [hasInvalidValue, propsSelection, restoreSelectionFromProps])
383
391
 
384
- // Store reference to original apply function (see below for usage in useEffect)
385
- const originalApply = useMemo(() => slateEditor.apply, [slateEditor])
386
-
387
392
  const [syncedRangeDecorations, setSyncedRangeDecorations] = useState(false)
388
393
  useEffect(() => {
389
394
  if (!syncedRangeDecorations) {
@@ -402,16 +407,9 @@ export const PortableTextEditable = forwardRef<
402
407
 
403
408
  // Sync range decorations after an operation is applied
404
409
  useEffect(() => {
405
- slateEditor.apply = (op: Operation) => {
406
- originalApply(op)
407
- if (op.type !== 'set_selection') {
408
- syncRangeDecorations(op)
409
- }
410
- }
411
- return () => {
412
- slateEditor.apply = originalApply
413
- }
414
- }, [originalApply, slateEditor, syncRangeDecorations])
410
+ const teardown = withSyncRangeDecorations(slateEditor, syncRangeDecorations)
411
+ return () => teardown()
412
+ }, [slateEditor, syncRangeDecorations])
415
413
 
416
414
  // Handle from props onCopy function
417
415
  const handleCopy = useCallback(
@@ -498,7 +496,7 @@ export const PortableTextEditable = forwardRef<
498
496
  Transforms.select(slateEditor, Editor.start(slateEditor, []))
499
497
  slateEditor.onChange()
500
498
  }
501
- editorActor.send({type: 'focus', event})
499
+ editorActor.send({type: 'focused', event})
502
500
  const newSelection = PortableTextEditor.getSelection(portableTextEditor)
503
501
  // If the selection is the same, emit it explicitly here as there is no actual onChange event triggered.
504
502
  if (selection === newSelection) {
@@ -13,6 +13,7 @@ import {
13
13
  type PropsWithChildren,
14
14
  } from 'react'
15
15
  import {Subject} from 'rxjs'
16
+ import {Slate} from 'slate-react'
16
17
  import {createActor} from 'xstate'
17
18
  import type {
18
19
  EditableAPI,
@@ -26,14 +27,17 @@ import type {
26
27
  import {debugWithName} from '../utils/debug'
27
28
  import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
28
29
  import {compileType} from '../utils/schema'
29
- import {SlateContainer} from './components/SlateContainer'
30
30
  import {Synchronizer} from './components/Synchronizer'
31
+ import {createSlateEditor, type SlateEditor} from './create-slate-editor'
31
32
  import {EditorActorContext} from './editor-actor-context'
32
33
  import {editorMachine, type EditorActor} from './editor-machine'
33
34
  import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
34
35
  import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection'
35
- import {PortableTextEditorReadOnlyContext} from './hooks/usePortableTextReadOnly'
36
36
  import {defaultKeyGenerator} from './key-generator'
37
+ import {
38
+ createEditableAPI,
39
+ type AddedAnnotationPaths,
40
+ } from './plugins/createWithEditableAPI'
37
41
  import type {Editor} from './use-editor'
38
42
 
39
43
  const debug = debugWithName('component:PortableTextEditor')
@@ -85,12 +89,12 @@ export type PortableTextEditorProps<
85
89
  * Backward compatibility (renamed to patches$).
86
90
  */
87
91
  incomingPatches$?: PatchObservable
88
- }) & {
89
- /**
90
- * Whether or not the editor should be in read-only mode
91
- */
92
- readOnly?: boolean
93
92
 
93
+ /**
94
+ * Whether or not the editor should be in read-only mode
95
+ */
96
+ readOnly?: boolean
97
+ }) & {
94
98
  /**
95
99
  * The current value of the portable text field
96
100
  */
@@ -124,12 +128,14 @@ export class PortableTextEditor extends Component<
124
128
  */
125
129
  private editable?: EditableAPI
126
130
  private editorActor: EditorActor
131
+ private slateEditor: SlateEditor
127
132
 
128
133
  constructor(props: PortableTextEditorProps) {
129
134
  super(props)
130
135
 
131
136
  if (props.editor) {
132
- this.editorActor = props.editor
137
+ const editor = props.editor as Editor
138
+ this.editorActor = editor._internal.editorActor
133
139
  this.editorActor.start()
134
140
  this.schemaTypes = this.editorActor.getSnapshot().context.schema
135
141
  } else {
@@ -149,16 +155,37 @@ export class PortableTextEditor extends Component<
149
155
  : compileType(props.schemaType),
150
156
  )
151
157
 
152
- this.editorActor =
153
- props.editor ??
154
- createActor(editorMachine, {
155
- input: {
156
- keyGenerator: props.keyGenerator || defaultKeyGenerator,
157
- schema: this.schemaTypes,
158
- },
159
- })
158
+ this.editorActor = createActor(editorMachine, {
159
+ input: {
160
+ keyGenerator: props.keyGenerator || defaultKeyGenerator,
161
+ schema: this.schemaTypes,
162
+ },
163
+ })
160
164
  this.editorActor.start()
165
+
166
+ if (props.readOnly) {
167
+ this.editorActor.send({
168
+ type: 'toggle readOnly',
169
+ })
170
+ }
171
+
172
+ if (props.maxBlocks) {
173
+ this.editorActor.send({
174
+ type: 'update maxBlocks',
175
+ maxBlocks:
176
+ props.maxBlocks === undefined
177
+ ? undefined
178
+ : Number.parseInt(props.maxBlocks.toString(), 10),
179
+ })
180
+ }
161
181
  }
182
+ this.slateEditor = createSlateEditor({
183
+ editorActor: this.editorActor,
184
+ })
185
+ this.editable = createEditableAPI(
186
+ this.slateEditor.instance,
187
+ this.editorActor,
188
+ )
162
189
  }
163
190
 
164
191
  componentDidUpdate(prevProps: PortableTextEditorProps) {
@@ -180,11 +207,33 @@ export class PortableTextEditor extends Component<
180
207
  })
181
208
  }
182
209
 
210
+ if (!this.props.editor && !prevProps.editor) {
211
+ if (this.props.readOnly !== prevProps.readOnly) {
212
+ this.editorActor.send({
213
+ type: 'toggle readOnly',
214
+ })
215
+ }
216
+
217
+ if (this.props.maxBlocks !== prevProps.maxBlocks) {
218
+ this.editorActor.send({
219
+ type: 'update maxBlocks',
220
+ maxBlocks:
221
+ this.props.maxBlocks === undefined
222
+ ? undefined
223
+ : Number.parseInt(this.props.maxBlocks.toString(), 10),
224
+ })
225
+ }
226
+ }
227
+
183
228
  if (this.props.editorRef !== prevProps.editorRef && this.props.editorRef) {
184
229
  this.props.editorRef.current = this
185
230
  }
186
231
  }
187
232
 
233
+ componentWillUnmount(): void {
234
+ this.slateEditor.destroy()
235
+ }
236
+
188
237
  public setEditable = (editable: EditableAPI) => {
189
238
  this.editable = {...this.editable, ...editable}
190
239
  }
@@ -198,13 +247,6 @@ export class PortableTextEditor extends Component<
198
247
  }
199
248
 
200
249
  render() {
201
- const maxBlocks = !this.props.editor
202
- ? typeof this.props.maxBlocks === 'undefined'
203
- ? undefined
204
- : Number.parseInt(this.props.maxBlocks.toString(), 10) || undefined
205
- : undefined
206
-
207
- const readOnly = Boolean(this.props.readOnly)
208
250
  const legacyPatches = !this.props.editor
209
251
  ? (this.props.incomingPatches$ ?? this.props.patches$)
210
252
  : undefined
@@ -218,37 +260,33 @@ export class PortableTextEditor extends Component<
218
260
  />
219
261
  ) : null}
220
262
  <EditorActorContext.Provider value={this.editorActor}>
221
- <SlateContainer
222
- editorActor={this.editorActor}
223
- maxBlocks={maxBlocks}
224
- portableTextEditor={this}
225
- readOnly={readOnly}
263
+ <Slate
264
+ editor={this.slateEditor.instance}
265
+ initialValue={this.slateEditor.initialValue}
226
266
  >
227
267
  <PortableTextEditorContext.Provider value={this}>
228
- <PortableTextEditorReadOnlyContext.Provider value={readOnly}>
229
- <PortableTextEditorSelectionProvider
268
+ <PortableTextEditorSelectionProvider
269
+ editorActor={this.editorActor}
270
+ >
271
+ <Synchronizer
230
272
  editorActor={this.editorActor}
231
- >
232
- <Synchronizer
233
- editorActor={this.editorActor}
234
- getValue={this.getValue}
235
- onChange={(change) => {
236
- if (!this.props.editor) {
237
- this.props.onChange(change)
238
- }
239
- /**
240
- * For backwards compatibility, we relay all changes to the
241
- * `change$` Subject as well.
242
- */
243
- this.change$.next(change)
244
- }}
245
- value={this.props.value}
246
- />
247
- {this.props.children}
248
- </PortableTextEditorSelectionProvider>
249
- </PortableTextEditorReadOnlyContext.Provider>
273
+ getValue={this.getValue}
274
+ onChange={(change) => {
275
+ if (!this.props.editor) {
276
+ this.props.onChange(change)
277
+ }
278
+ /**
279
+ * For backwards compatibility, we relay all changes to the
280
+ * `change$` Subject as well.
281
+ */
282
+ this.change$.next(change)
283
+ }}
284
+ value={this.props.value}
285
+ />
286
+ {this.props.children}
287
+ </PortableTextEditorSelectionProvider>
250
288
  </PortableTextEditorContext.Provider>
251
- </SlateContainer>
289
+ </Slate>
252
290
  </EditorActorContext.Provider>
253
291
  </>
254
292
  )
@@ -272,22 +310,8 @@ export class PortableTextEditor extends Component<
272
310
  editor: PortableTextEditor,
273
311
  type: TSchemaType,
274
312
  value?: {[prop: string]: unknown},
275
- ):
276
- | {
277
- /**
278
- * @deprecated An annotation may be applied to multiple blocks, resulting
279
- * in multiple `markDef`'s being created. Use `markDefPaths` instead.
280
- */
281
- markDefPath: Path
282
- markDefPaths: Array<Path>
283
- /**
284
- * @deprecated Does not return anything meaningful since an annotation
285
- * can span multiple blocks and spans. If references the span closest
286
- * to the focus point of the selection.
287
- */
288
- spanPath: Path
289
- }
290
- | undefined => editor.editable?.addAnnotation(type, value)
313
+ ): AddedAnnotationPaths | undefined =>
314
+ editor.editable?.addAnnotation(type, value)
291
315
  static blur = (editor: PortableTextEditor): void => {
292
316
  debug('Host blurred')
293
317
  editor.editable?.blur()
@@ -2,10 +2,9 @@ import {isEqual} from 'lodash'
2
2
  import {Editor, Node, Path, Range, Transforms} from 'slate'
3
3
  import type {SlateTextBlock, VoidElement} from '../../types/slate'
4
4
  import type {BehaviourActionImplementation} from './behavior.actions'
5
- import type {BehaviorAction, PickFromUnion} from './behavior.types'
6
5
 
7
6
  export const insertBreakActionImplementation: BehaviourActionImplementation<
8
- PickFromUnion<BehaviorAction, 'type', 'insert break' | 'insert soft break'>
7
+ 'insert break'
9
8
  > = ({context, action}) => {
10
9
  const keyGenerator = context.keyGenerator
11
10
  const schema = context.schema
@@ -204,3 +203,14 @@ export const insertBreakActionImplementation: BehaviourActionImplementation<
204
203
  }
205
204
  }
206
205
  }
206
+
207
+ export const insertSoftBreakActionImplementation: BehaviourActionImplementation<
208
+ 'insert soft break'
209
+ > = ({context, action}) => {
210
+ // This mimics Slate's internal which also just does a regular insert break
211
+ // when soft-breaking
212
+ insertBreakActionImplementation({
213
+ context,
214
+ action: {...action, type: 'insert break'},
215
+ })
216
+ }
@@ -5,14 +5,23 @@ import {
5
5
  insertText,
6
6
  Transforms,
7
7
  } from 'slate'
8
+ import {ReactEditor} from 'slate-react'
8
9
  import type {PortableTextMemberSchemaTypes} from '../../types/editor'
9
10
  import {toSlateRange} from '../../utils/ranges'
11
+ import {
12
+ addAnnotationActionImplementation,
13
+ removeAnnotationActionImplementation,
14
+ toggleAnnotationActionImplementation,
15
+ } from '../plugins/createWithEditableAPI'
10
16
  import {
11
17
  addDecoratorActionImplementation,
12
18
  removeDecoratorActionImplementation,
13
19
  toggleDecoratorActionImplementation,
14
20
  } from '../plugins/createWithPortableTextMarkModel'
15
- import {insertBreakActionImplementation} from './behavior.action.insert-break'
21
+ import {
22
+ insertBreakActionImplementation,
23
+ insertSoftBreakActionImplementation,
24
+ } from './behavior.action.insert-break'
16
25
  import type {
17
26
  BehaviorAction,
18
27
  BehaviorEvent,
@@ -25,25 +34,30 @@ export type BehaviorActionContext = {
25
34
  }
26
35
 
27
36
  export type BehaviourActionImplementation<
28
- TBehaviorAction extends BehaviorAction,
37
+ TBehaviorActionType extends BehaviorAction['type'],
38
+ TReturnType = void,
29
39
  > = ({
30
40
  context,
31
41
  action,
32
42
  }: {
33
43
  context: BehaviorActionContext
34
- action: TBehaviorAction
35
- }) => void
44
+ action: PickFromUnion<BehaviorAction, 'type', TBehaviorActionType>
45
+ }) => TReturnType
36
46
 
37
47
  type BehaviourActionImplementations = {
38
- [TBehaviorActionType in BehaviorAction['type']]: BehaviourActionImplementation<
39
- PickFromUnion<BehaviorAction, 'type', TBehaviorActionType>
40
- >
48
+ [TBehaviorActionType in BehaviorAction['type']]: BehaviourActionImplementation<TBehaviorActionType>
41
49
  }
42
50
 
43
51
  const behaviorActionImplementations: BehaviourActionImplementations = {
52
+ 'annotation.add': addAnnotationActionImplementation,
53
+ 'annotation.remove': removeAnnotationActionImplementation,
54
+ 'annotation.toggle': toggleAnnotationActionImplementation,
44
55
  'decorator.add': addDecoratorActionImplementation,
45
56
  'decorator.remove': removeDecoratorActionImplementation,
46
57
  'decorator.toggle': toggleDecoratorActionImplementation,
58
+ 'focus': ({action}) => {
59
+ ReactEditor.focus(action.editor)
60
+ },
47
61
  'set block': ({action}) => {
48
62
  for (const path of action.paths) {
49
63
  const at = toSlateRange(
@@ -99,9 +113,7 @@ const behaviorActionImplementations: BehaviourActionImplementations = {
99
113
  }
100
114
  },
101
115
  'insert break': insertBreakActionImplementation,
102
- // This mimics Slate's internal which also just does a regular insert break
103
- // when on soft break
104
- 'insert soft break': insertBreakActionImplementation,
116
+ 'insert soft break': insertSoftBreakActionImplementation,
105
117
  'insert text': ({action}) => {
106
118
  insertText(action.editor, action.text)
107
119
  },
@@ -205,7 +217,7 @@ export function performAction({
205
217
  }
206
218
  }
207
219
 
208
- export function performDefaultAction({
220
+ function performDefaultAction({
209
221
  context,
210
222
  action,
211
223
  }: {
@@ -213,6 +225,27 @@ export function performDefaultAction({
213
225
  action: PickFromUnion<BehaviorAction, 'type', BehaviorEvent['type']>
214
226
  }) {
215
227
  switch (action.type) {
228
+ case 'annotation.add': {
229
+ behaviorActionImplementations['annotation.add']({
230
+ context,
231
+ action,
232
+ })
233
+ break
234
+ }
235
+ case 'annotation.remove': {
236
+ behaviorActionImplementations['annotation.remove']({
237
+ context,
238
+ action,
239
+ })
240
+ break
241
+ }
242
+ case 'annotation.toggle': {
243
+ behaviorActionImplementations['annotation.toggle']({
244
+ context,
245
+ action,
246
+ })
247
+ break
248
+ }
216
249
  case 'decorator.add': {
217
250
  behaviorActionImplementations['decorator.add']({
218
251
  context,
@@ -248,6 +281,13 @@ export function performDefaultAction({
248
281
  })
249
282
  break
250
283
  }
284
+ case 'focus': {
285
+ behaviorActionImplementations.focus({
286
+ context,
287
+ action,
288
+ })
289
+ break
290
+ }
251
291
  case 'insert break': {
252
292
  behaviorActionImplementations['insert break']({
253
293
  context,
@@ -20,6 +20,26 @@ export type BehaviorContext = {
20
20
  * @alpha
21
21
  */
22
22
  export type BehaviorEvent =
23
+ | {
24
+ type: 'annotation.add'
25
+ annotation: {
26
+ name: string
27
+ value: {[prop: string]: unknown}
28
+ }
29
+ }
30
+ | {
31
+ type: 'annotation.remove'
32
+ annotation: {
33
+ name: string
34
+ }
35
+ }
36
+ | {
37
+ type: 'annotation.toggle'
38
+ annotation: {
39
+ name: string
40
+ value: {[prop: string]: unknown}
41
+ }
42
+ }
23
43
  | {
24
44
  type: 'decorator.add'
25
45
  decorator: string
@@ -40,6 +60,9 @@ export type BehaviorEvent =
40
60
  type: 'delete forward'
41
61
  unit: TextUnit
42
62
  }
63
+ | {
64
+ type: 'focus'
65
+ }
43
66
  | {
44
67
  type: 'insert soft break'
45
68
  }
@@ -1,5 +1,6 @@
1
1
  import type {Patch} from '@portabletext/patches'
2
2
  import type {PortableTextBlock} from '@sanity/types'
3
+ import {useSelector} from '@xstate/react'
3
4
  import {throttle} from 'lodash'
4
5
  import {useCallback, useEffect, useRef} from 'react'
5
6
  import {Editor} from 'slate'
@@ -10,7 +11,6 @@ import {debugWithName} from '../../utils/debug'
10
11
  import {IS_PROCESSING_LOCAL_CHANGES} from '../../utils/weakMaps'
11
12
  import type {EditorActor} from '../editor-machine'
12
13
  import {usePortableTextEditor} from '../hooks/usePortableTextEditor'
13
- import {usePortableTextEditorReadOnlyStatus} from '../hooks/usePortableTextReadOnly'
14
14
  import {useSyncValue} from '../hooks/useSyncValue'
15
15
 
16
16
  const debug = debugWithName('component:PortableTextEditor:Synchronizer')
@@ -36,7 +36,7 @@ export interface SynchronizerProps {
36
36
  */
37
37
  export function Synchronizer(props: SynchronizerProps) {
38
38
  const portableTextEditor = usePortableTextEditor()
39
- const readOnly = usePortableTextEditorReadOnlyStatus()
39
+ const readOnly = useSelector(props.editorActor, (s) => s.context.readOnly)
40
40
  const {editorActor, getValue, onChange, value} = props
41
41
  const pendingPatches = useRef<Patch[]>([])
42
42
 
@@ -121,6 +121,10 @@ export function Synchronizer(props: SynchronizerProps) {
121
121
  handleChange({type: 'loading', isLoading: false})
122
122
  break
123
123
  }
124
+ case 'focused': {
125
+ handleChange({type: 'focus', event: event.event})
126
+ break
127
+ }
124
128
  case 'offline': {
125
129
  handleChange({type: 'connection', value: 'offline'})
126
130
  break
@@ -148,9 +152,12 @@ export function Synchronizer(props: SynchronizerProps) {
148
152
  })
149
153
  break
150
154
  }
151
- case 'patches': {
155
+ case 'annotation.add':
156
+ case 'annotation.remove':
157
+ case 'annotation.toggle':
158
+ case 'focus':
159
+ case 'patches':
152
160
  break
153
- }
154
161
  default:
155
162
  handleChange(event)
156
163
  }