@portabletext/editor 1.6.1 → 1.7.1

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 (36) hide show
  1. package/README.md +5 -0
  2. package/lib/index.d.mts +3327 -3489
  3. package/lib/index.d.ts +3327 -3489
  4. package/lib/index.esm.js +4319 -4079
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +4311 -4071
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +4319 -4079
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +18 -19
  11. package/src/editor/Editable.tsx +5 -4
  12. package/src/editor/PortableTextEditor.tsx +88 -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 +72 -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 +50 -14
  30. package/src/index.ts +10 -1
  31. package/src/types/editor.ts +0 -1
  32. package/src/types/options.ts +1 -3
  33. package/src/utils/__tests__/operationToPatches.test.ts +7 -14
  34. package/src/utils/__tests__/patchToOperations.test.ts +4 -7
  35. package/src/editor/components/SlateContainer.tsx +0 -79
  36. 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.1",
3
+ "version": "1.7.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -43,26 +43,26 @@
43
43
  ],
44
44
  "dependencies": {
45
45
  "@portabletext/patches": "1.1.0",
46
- "@xstate/react": "^4.1.3",
46
+ "@xstate/react": "^5.0.0",
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",
55
56
  "use-effect-event": "^1.0.2",
56
- "xstate": "^5.18.2"
57
+ "xstate": "^5.19.0"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@portabletext/toolkit": "^2.0.16",
60
- "@sanity/block-tools": "^3.63.0",
61
+ "@sanity/block-tools": "^3.64.0",
61
62
  "@sanity/diff-match-patch": "^3.1.1",
62
- "@sanity/pkg-utils": "^6.11.9",
63
- "@sanity/schema": "^3.63.0",
64
- "@sanity/types": "^3.63.0",
65
- "@sanity/util": "^3.63.0",
63
+ "@sanity/pkg-utils": "^6.11.10",
64
+ "@sanity/schema": "^3.64.0",
65
+ "@sanity/types": "^3.64.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,16 +84,15 @@
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"
91
91
  },
92
92
  "peerDependencies": {
93
- "@sanity/block-tools": "^3.63.0",
94
- "@sanity/schema": "^3.63.0",
95
- "@sanity/types": "^3.63.0",
96
- "@sanity/util": "^3.63.0",
93
+ "@sanity/block-tools": "^3.64.0",
94
+ "@sanity/schema": "^3.64.0",
95
+ "@sanity/types": "^3.64.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,8 +67,8 @@ 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'
72
73
  import {withSyncRangeDecorations} from './withSyncRangeDecorations'
73
74
 
@@ -140,7 +141,6 @@ export const PortableTextEditable = forwardRef<
140
141
  } = props
141
142
 
142
143
  const portableTextEditor = usePortableTextEditor()
143
- const readOnly = usePortableTextEditorReadOnlyStatus()
144
144
  const ref = useRef<HTMLDivElement | null>(null)
145
145
  const [editableElement, setEditableElement] = useState<HTMLDivElement | null>(
146
146
  null,
@@ -159,6 +159,7 @@ export const PortableTextEditable = forwardRef<
159
159
  const rangeDecorationsRef = useRef(rangeDecorations)
160
160
 
161
161
  const editorActor = useContext(EditorActorContext)
162
+ const readOnly = useSelector(editorActor, (s) => s.context.readOnly)
162
163
  const {schemaTypes} = portableTextEditor
163
164
  const slateEditor = useSlate()
164
165
 
@@ -495,7 +496,7 @@ export const PortableTextEditable = forwardRef<
495
496
  Transforms.select(slateEditor, Editor.start(slateEditor, []))
496
497
  slateEditor.onChange()
497
498
  }
498
- editorActor.send({type: 'focus', event})
499
+ editorActor.send({type: 'focused', event})
499
500
  const newSelection = PortableTextEditor.getSelection(portableTextEditor)
500
501
  // If the selection is the same, emit it explicitly here as there is no actual onChange event triggered.
501
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,15 @@ 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
139
+ this.slateEditor = editor._internal.slateEditor
133
140
  this.editorActor.start()
134
141
  this.schemaTypes = this.editorActor.getSnapshot().context.schema
135
142
  } else {
@@ -149,16 +156,38 @@ export class PortableTextEditor extends Component<
149
156
  : compileType(props.schemaType),
150
157
  )
151
158
 
152
- this.editorActor =
153
- props.editor ??
154
- createActor(editorMachine, {
155
- input: {
156
- keyGenerator: props.keyGenerator || defaultKeyGenerator,
157
- schema: this.schemaTypes,
158
- },
159
- })
159
+ this.editorActor = createActor(editorMachine, {
160
+ input: {
161
+ keyGenerator: props.keyGenerator || defaultKeyGenerator,
162
+ schema: this.schemaTypes,
163
+ },
164
+ })
160
165
  this.editorActor.start()
166
+
167
+ this.slateEditor = createSlateEditor({
168
+ editorActor: this.editorActor,
169
+ })
170
+
171
+ if (props.readOnly) {
172
+ this.editorActor.send({
173
+ type: 'toggle readOnly',
174
+ })
175
+ }
176
+
177
+ if (props.maxBlocks) {
178
+ this.editorActor.send({
179
+ type: 'update maxBlocks',
180
+ maxBlocks:
181
+ props.maxBlocks === undefined
182
+ ? undefined
183
+ : Number.parseInt(props.maxBlocks.toString(), 10),
184
+ })
185
+ }
161
186
  }
187
+ this.editable = createEditableAPI(
188
+ this.slateEditor.instance,
189
+ this.editorActor,
190
+ )
162
191
  }
163
192
 
164
193
  componentDidUpdate(prevProps: PortableTextEditorProps) {
@@ -180,6 +209,24 @@ export class PortableTextEditor extends Component<
180
209
  })
181
210
  }
182
211
 
212
+ if (!this.props.editor && !prevProps.editor) {
213
+ if (this.props.readOnly !== prevProps.readOnly) {
214
+ this.editorActor.send({
215
+ type: 'toggle readOnly',
216
+ })
217
+ }
218
+
219
+ if (this.props.maxBlocks !== prevProps.maxBlocks) {
220
+ this.editorActor.send({
221
+ type: 'update maxBlocks',
222
+ maxBlocks:
223
+ this.props.maxBlocks === undefined
224
+ ? undefined
225
+ : Number.parseInt(this.props.maxBlocks.toString(), 10),
226
+ })
227
+ }
228
+ }
229
+
183
230
  if (this.props.editorRef !== prevProps.editorRef && this.props.editorRef) {
184
231
  this.props.editorRef.current = this
185
232
  }
@@ -198,13 +245,6 @@ export class PortableTextEditor extends Component<
198
245
  }
199
246
 
200
247
  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
248
  const legacyPatches = !this.props.editor
209
249
  ? (this.props.incomingPatches$ ?? this.props.patches$)
210
250
  : undefined
@@ -218,37 +258,33 @@ export class PortableTextEditor extends Component<
218
258
  />
219
259
  ) : null}
220
260
  <EditorActorContext.Provider value={this.editorActor}>
221
- <SlateContainer
222
- editorActor={this.editorActor}
223
- maxBlocks={maxBlocks}
224
- portableTextEditor={this}
225
- readOnly={readOnly}
261
+ <Slate
262
+ editor={this.slateEditor.instance}
263
+ initialValue={this.slateEditor.initialValue}
226
264
  >
227
265
  <PortableTextEditorContext.Provider value={this}>
228
- <PortableTextEditorReadOnlyContext.Provider value={readOnly}>
229
- <PortableTextEditorSelectionProvider
266
+ <PortableTextEditorSelectionProvider
267
+ editorActor={this.editorActor}
268
+ >
269
+ <Synchronizer
230
270
  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>
271
+ getValue={this.getValue}
272
+ onChange={(change) => {
273
+ if (!this.props.editor) {
274
+ this.props.onChange(change)
275
+ }
276
+ /**
277
+ * For backwards compatibility, we relay all changes to the
278
+ * `change$` Subject as well.
279
+ */
280
+ this.change$.next(change)
281
+ }}
282
+ value={this.props.value}
283
+ />
284
+ {this.props.children}
285
+ </PortableTextEditorSelectionProvider>
250
286
  </PortableTextEditorContext.Provider>
251
- </SlateContainer>
287
+ </Slate>
252
288
  </EditorActorContext.Provider>
253
289
  </>
254
290
  )
@@ -272,22 +308,8 @@ export class PortableTextEditor extends Component<
272
308
  editor: PortableTextEditor,
273
309
  type: TSchemaType,
274
310
  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)
311
+ ): AddedAnnotationPaths | undefined =>
312
+ editor.editable?.addAnnotation(type, value)
291
313
  static blur = (editor: PortableTextEditor): void => {
292
314
  debug('Host blurred')
293
315
  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
  }
@@ -0,0 +1,72 @@
1
+ import {createEditor, type Descendant} from 'slate'
2
+ import {withReact} from 'slate-react'
3
+ import type {PortableTextSlateEditor} from '../types/editor'
4
+ import {debugWithName} from '../utils/debug'
5
+ import {KEY_TO_SLATE_ELEMENT, KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
6
+ import type {EditorActor} from './editor-machine'
7
+ import {withPlugins} from './plugins/with-plugins'
8
+
9
+ const debug = debugWithName('component:PortableTextEditor:SlateContainer')
10
+
11
+ type SlateEditorConfig = {
12
+ editorActor: EditorActor
13
+ }
14
+
15
+ /**
16
+ * @internal
17
+ */
18
+ export type SlateEditor = {
19
+ instance: PortableTextSlateEditor
20
+ initialValue: Array<Descendant>
21
+ }
22
+
23
+ const slateEditors = new WeakMap<EditorActor, SlateEditor>()
24
+
25
+ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
26
+ const existingSlateEditor = slateEditors.get(config.editorActor)
27
+
28
+ if (existingSlateEditor) {
29
+ debug('Reusing existing Slate editor instance', config.editorActor.id)
30
+ return existingSlateEditor
31
+ }
32
+
33
+ debug('Creating new Slate editor instance', config.editorActor.id)
34
+
35
+ let unsubscriptions: Array<() => void> = []
36
+ let subscriptions: Array<() => () => void> = []
37
+
38
+ const instance = withPlugins(withReact(createEditor()), {
39
+ editorActor: config.editorActor,
40
+ subscriptions,
41
+ })
42
+
43
+ KEY_TO_VALUE_ELEMENT.set(instance, {})
44
+ KEY_TO_SLATE_ELEMENT.set(instance, {})
45
+
46
+ for (const subscription of subscriptions) {
47
+ unsubscriptions.push(subscription())
48
+ }
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
+ const initialValue = [instance.pteCreateTextBlock({decorators: []})]
63
+
64
+ const slateEditor: SlateEditor = {
65
+ instance,
66
+ initialValue,
67
+ }
68
+
69
+ slateEditors.set(config.editorActor, slateEditor)
70
+
71
+ return slateEditor
72
+ }