@portabletext/editor 1.6.1 → 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 (36) 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 +4316 -4081
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +4307 -4072
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +4316 -4081
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +10 -11
  11. package/src/editor/Editable.tsx +5 -4
  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/index.ts +9 -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.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,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,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
  }
@@ -0,0 +1,67 @@
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
+ export type SlateEditor = {
16
+ instance: PortableTextSlateEditor
17
+ initialValue: Array<Descendant>
18
+ destroy: () => void
19
+ }
20
+
21
+ const slateEditors = new WeakMap<EditorActor, SlateEditor>()
22
+
23
+ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
24
+ const existingSlateEditor = slateEditors.get(config.editorActor)
25
+
26
+ if (existingSlateEditor) {
27
+ debug('Reusing existing Slate editor instance', config.editorActor.id)
28
+ return existingSlateEditor
29
+ }
30
+
31
+ debug('Creating new Slate editor instance', config.editorActor.id)
32
+
33
+ let unsubscriptions: Array<() => void> = []
34
+ let subscriptions: Array<() => () => void> = []
35
+
36
+ const instance = withPlugins(withReact(createEditor()), {
37
+ editorActor: config.editorActor,
38
+ subscriptions,
39
+ })
40
+
41
+ KEY_TO_VALUE_ELEMENT.set(instance, {})
42
+ KEY_TO_SLATE_ELEMENT.set(instance, {})
43
+
44
+ for (const subscription of subscriptions) {
45
+ unsubscriptions.push(subscription())
46
+ }
47
+
48
+ const initialValue = [instance.pteCreateTextBlock({decorators: []})]
49
+
50
+ const slateEditor: SlateEditor = {
51
+ instance,
52
+ initialValue,
53
+ destroy: () => {
54
+ debug('Destroying Slate editor')
55
+ instance.destroy()
56
+ for (const unsubscribe of unsubscriptions) {
57
+ unsubscribe()
58
+ }
59
+ subscriptions = []
60
+ unsubscriptions = []
61
+ },
62
+ }
63
+
64
+ slateEditors.set(config.editorActor, slateEditor)
65
+
66
+ return slateEditor
67
+ }