@portabletext/editor 1.3.0 → 1.3.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -67,7 +67,7 @@
67
67
  "@sanity/ui": "^2.8.17",
68
68
  "@sanity/util": "^3.62.3",
69
69
  "@testing-library/dom": "^10.4.0",
70
- "@testing-library/jest-dom": "^6.6.2",
70
+ "@testing-library/jest-dom": "^6.6.3",
71
71
  "@testing-library/react": "^16.0.1",
72
72
  "@testing-library/user-event": "^14.5.2",
73
73
  "@types/debug": "^4.1.5",
@@ -9,7 +9,12 @@ import type {
9
9
  PortableTextObject,
10
10
  SpanSchemaType,
11
11
  } from '@sanity/types'
12
- import {Component, type MutableRefObject, type PropsWithChildren} from 'react'
12
+ import {
13
+ Component,
14
+ useEffect,
15
+ type MutableRefObject,
16
+ type PropsWithChildren,
17
+ } from 'react'
13
18
  import {Subject} from 'rxjs'
14
19
  import {createActor} from 'xstate'
15
20
  import type {
@@ -181,38 +186,45 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
181
186
  const readOnly = Boolean(this.props.readOnly)
182
187
 
183
188
  return (
184
- <EditorActorContext.Provider value={this.editorActor}>
185
- <SlateContainer
186
- editorActor={this.editorActor}
187
- maxBlocks={maxBlocks}
188
- patches$={_patches$}
189
- portableTextEditor={this}
190
- readOnly={readOnly}
191
- >
192
- <PortableTextEditorContext.Provider value={this}>
193
- <PortableTextEditorReadOnlyContext.Provider value={readOnly}>
194
- <PortableTextEditorSelectionProvider
195
- editorActor={this.editorActor}
196
- >
197
- <Synchronizer
189
+ <>
190
+ {_patches$ ? (
191
+ <RoutePatchesObservableToEditorActor
192
+ editorActor={this.editorActor}
193
+ patches$={_patches$}
194
+ />
195
+ ) : null}
196
+ <EditorActorContext.Provider value={this.editorActor}>
197
+ <SlateContainer
198
+ editorActor={this.editorActor}
199
+ maxBlocks={maxBlocks}
200
+ portableTextEditor={this}
201
+ readOnly={readOnly}
202
+ >
203
+ <PortableTextEditorContext.Provider value={this}>
204
+ <PortableTextEditorReadOnlyContext.Provider value={readOnly}>
205
+ <PortableTextEditorSelectionProvider
198
206
  editorActor={this.editorActor}
199
- getValue={this.getValue}
200
- onChange={(change) => {
201
- this.props.onChange(change)
202
- /**
203
- * For backwards compatibility, we relay all changes to the
204
- * `change$` Subject as well.
205
- */
206
- this.change$.next(change)
207
- }}
208
- value={value}
209
- />
210
- {children}
211
- </PortableTextEditorSelectionProvider>
212
- </PortableTextEditorReadOnlyContext.Provider>
213
- </PortableTextEditorContext.Provider>
214
- </SlateContainer>
215
- </EditorActorContext.Provider>
207
+ >
208
+ <Synchronizer
209
+ editorActor={this.editorActor}
210
+ getValue={this.getValue}
211
+ onChange={(change) => {
212
+ this.props.onChange(change)
213
+ /**
214
+ * For backwards compatibility, we relay all changes to the
215
+ * `change$` Subject as well.
216
+ */
217
+ this.change$.next(change)
218
+ }}
219
+ value={value}
220
+ />
221
+ {children}
222
+ </PortableTextEditorSelectionProvider>
223
+ </PortableTextEditorReadOnlyContext.Provider>
224
+ </PortableTextEditorContext.Provider>
225
+ </SlateContainer>
226
+ </EditorActorContext.Provider>
227
+ </>
216
228
  )
217
229
  }
218
230
 
@@ -378,3 +390,23 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
378
390
  return editor.editable?.isSelectionsOverlapping(selectionA, selectionB)
379
391
  }
380
392
  }
393
+
394
+ function RoutePatchesObservableToEditorActor(props: {
395
+ editorActor: EditorActor
396
+ patches$: PatchObservable
397
+ }) {
398
+ useEffect(() => {
399
+ const subscription = props.patches$.subscribe((payload) => {
400
+ props.editorActor.send({
401
+ type: 'patches',
402
+ ...payload,
403
+ })
404
+ })
405
+
406
+ return () => {
407
+ subscription.unsubscribe()
408
+ }
409
+ }, [props.editorActor, props.patches$])
410
+
411
+ return null
412
+ }
@@ -1,7 +1,6 @@
1
1
  import {useEffect, useMemo, useState, type PropsWithChildren} from 'react'
2
2
  import {createEditor} from 'slate'
3
3
  import {Slate, withReact} from 'slate-react'
4
- import type {PatchObservable} from '../../types/editor'
5
4
  import {debugWithName} from '../../utils/debug'
6
5
  import {KEY_TO_SLATE_ELEMENT, KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
7
6
  import type {EditorActor} from '../editor-machine'
@@ -16,7 +15,6 @@ const debug = debugWithName('component:PortableTextEditor:SlateContainer')
16
15
  export interface SlateContainerProps extends PropsWithChildren {
17
16
  editorActor: EditorActor
18
17
  maxBlocks: number | undefined
19
- patches$?: PatchObservable
20
18
  portableTextEditor: PortableTextEditor
21
19
  readOnly: boolean
22
20
  }
@@ -26,7 +24,7 @@ export interface SlateContainerProps extends PropsWithChildren {
26
24
  * @internal
27
25
  */
28
26
  export function SlateContainer(props: SlateContainerProps) {
29
- const {editorActor, patches$, portableTextEditor, readOnly, maxBlocks} = props
27
+ const {editorActor, portableTextEditor, readOnly, maxBlocks} = props
30
28
 
31
29
  // Create the slate instance, using `useState` ensures setup is only run once, initially
32
30
  const [[slateEditor, subscribe]] = useState(() => {
@@ -34,7 +32,6 @@ export function SlateContainer(props: SlateContainerProps) {
34
32
  const {editor, subscribe: _sub} = withPlugins(withReact(createEditor()), {
35
33
  editorActor,
36
34
  maxBlocks,
37
- patches$,
38
35
  portableTextEditor,
39
36
  readOnly,
40
37
  })
@@ -56,18 +53,10 @@ export function SlateContainer(props: SlateContainerProps) {
56
53
  withPlugins(slateEditor, {
57
54
  editorActor,
58
55
  maxBlocks,
59
- patches$,
60
56
  portableTextEditor,
61
57
  readOnly,
62
58
  })
63
- }, [
64
- editorActor,
65
- portableTextEditor,
66
- maxBlocks,
67
- readOnly,
68
- patches$,
69
- slateEditor,
70
- ])
59
+ }, [editorActor, portableTextEditor, maxBlocks, readOnly, slateEditor])
71
60
 
72
61
  const initialValue = useMemo(() => {
73
62
  return [slateEditor.pteCreateTextBlock({decorators: []})]
@@ -1,7 +1,7 @@
1
1
  import type {Patch} from '@portabletext/patches'
2
2
  import type {PortableTextBlock} from '@sanity/types'
3
3
  import {throttle} from 'lodash'
4
- import {useCallback, useEffect, useMemo, useRef} from 'react'
4
+ import {useCallback, useEffect, useRef} from 'react'
5
5
  import {Editor} from 'slate'
6
6
  import {useSlate} from 'slate-react'
7
7
  import {useEffectEvent} from 'use-effect-event'
@@ -69,26 +69,6 @@ export function Synchronizer(props: SynchronizerProps) {
69
69
  IS_PROCESSING_LOCAL_CHANGES.set(slateEditor, false)
70
70
  }, [editorActor, slateEditor, getValue])
71
71
 
72
- const onFlushPendingPatchesThrottled = useMemo(() => {
73
- return throttle(
74
- () => {
75
- // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform,
76
- // and we can flush these changes immediately.
77
- if (Editor.isNormalizing(slateEditor)) {
78
- onFlushPendingPatches()
79
- return
80
- }
81
- // If it's in the middle of something, try again.
82
- onFlushPendingPatchesThrottled()
83
- },
84
- FLUSH_PATCHES_THROTTLED_MS,
85
- {
86
- leading: false,
87
- trailing: true,
88
- },
89
- )
90
- }, [onFlushPendingPatches, slateEditor])
91
-
92
72
  // Flush pending patches immediately on unmount
93
73
  useEffect(() => {
94
74
  return () => {
@@ -106,6 +86,24 @@ export function Synchronizer(props: SynchronizerProps) {
106
86
 
107
87
  // Subscribe to, and handle changes from the editor
108
88
  useEffect(() => {
89
+ const onFlushPendingPatchesThrottled = throttle(
90
+ () => {
91
+ // If the editor is normalizing (each operation) it means that it's not in the middle of a bigger transform,
92
+ // and we can flush these changes immediately.
93
+ if (Editor.isNormalizing(slateEditor)) {
94
+ onFlushPendingPatches()
95
+ return
96
+ }
97
+ // If it's in the middle of something, try again.
98
+ onFlushPendingPatchesThrottled()
99
+ },
100
+ FLUSH_PATCHES_THROTTLED_MS,
101
+ {
102
+ leading: false,
103
+ trailing: true,
104
+ },
105
+ )
106
+
109
107
  debug('Subscribing to editor changes')
110
108
  const sub = editorActor.on('*', (event) => {
111
109
  switch (event.type) {
@@ -150,6 +148,9 @@ export function Synchronizer(props: SynchronizerProps) {
150
148
  })
151
149
  break
152
150
  }
151
+ case 'patches': {
152
+ break
153
+ }
153
154
  default:
154
155
  handleChange(event)
155
156
  }
@@ -158,7 +159,7 @@ export function Synchronizer(props: SynchronizerProps) {
158
159
  debug('Unsubscribing to changes')
159
160
  sub.unsubscribe()
160
161
  }
161
- }, [handleChange, editorActor, onFlushPendingPatchesThrottled, slateEditor])
162
+ }, [editorActor, handleChange, onFlushPendingPatches, slateEditor])
162
163
 
163
164
  // Sync the value when going online
164
165
  const handleOnline = useCallback(() => {
@@ -168,16 +169,12 @@ export function Synchronizer(props: SynchronizerProps) {
168
169
 
169
170
  // Notify about window online and offline status changes
170
171
  useEffect(() => {
171
- const subscription = editorActor.on('online', () => {
172
- if (portableTextEditor.props.patches$) {
173
- handleOnline()
174
- }
175
- })
172
+ const subscription = editorActor.on('online', handleOnline)
176
173
 
177
174
  return () => {
178
175
  subscription.unsubscribe()
179
176
  }
180
- }, [handleOnline, editorActor, portableTextEditor.props.patches$])
177
+ }, [handleOnline, editorActor])
181
178
 
182
179
  // This hook must be set up after setting up the subscription above, or it will not pick up validation errors from the useSyncValue hook.
183
180
  // This will cause the editor to not be able to signal a validation error and offer invalid value resolution of the initial value.
@@ -56,6 +56,15 @@ const networkLogic = fromCallback(({sendBack}) => {
56
56
  */
57
57
  export type PatchEvent = {type: 'patch'; patch: Patch}
58
58
 
59
+ /**
60
+ * @internal
61
+ */
62
+ export type PatchesEvent = {
63
+ type: 'patches'
64
+ patches: Array<Patch>
65
+ snapshot: Array<PortableTextBlock> | undefined
66
+ }
67
+
59
68
  /**
60
69
  * @internal
61
70
  */
@@ -87,6 +96,7 @@ type EditorEvent =
87
96
  type EditorEmittedEvent =
88
97
  | {type: 'ready'}
89
98
  | PatchEvent
99
+ | PatchesEvent
90
100
  | MutationEvent
91
101
  | {
92
102
  type: 'unset'
@@ -274,6 +284,7 @@ export const editorMachine = setup({
274
284
  'online': {actions: emit({type: 'online'})},
275
285
  'offline': {actions: emit({type: 'offline'})},
276
286
  'loading': {actions: emit({type: 'loading'})},
287
+ 'patches': {actions: emit(({event}) => event)},
277
288
  'done loading': {actions: emit({type: 'done loading'})},
278
289
  'update schema': {actions: 'assign schema'},
279
290
  'behavior event': {actions: 'handle behavior event'},
@@ -13,7 +13,6 @@ import {
13
13
  type SplitNodeOperation,
14
14
  } from 'slate'
15
15
  import type {
16
- PatchObservable,
17
16
  PortableTextMemberSchemaTypes,
18
17
  PortableTextSlateEditor,
19
18
  } from '../../types/editor'
@@ -81,7 +80,6 @@ export interface PatchFunctions {
81
80
 
82
81
  interface Options {
83
82
  editorActor: EditorActor
84
- patches$?: PatchObservable
85
83
  patchFunctions: PatchFunctions
86
84
  readOnly: boolean
87
85
  schemaTypes: PortableTextMemberSchemaTypes
@@ -89,7 +87,6 @@ interface Options {
89
87
 
90
88
  export function createWithPatches({
91
89
  editorActor,
92
- patches$,
93
90
  patchFunctions,
94
91
  readOnly,
95
92
  schemaTypes,
@@ -143,16 +140,14 @@ export function createWithPatches({
143
140
  handleBufferedRemotePatches()
144
141
  }
145
142
 
146
- if (patches$) {
147
- editor.subscriptions.push(() => {
148
- debug('Subscribing to patches$')
149
- const sub = patches$.subscribe(handlePatches)
150
- return () => {
151
- debug('Unsubscribing to patches$')
152
- sub.unsubscribe()
153
- }
154
- })
155
- }
143
+ editor.subscriptions.push(() => {
144
+ debug('Subscribing to remote patches')
145
+ const sub = editorActor.on('patches', handlePatches)
146
+ return () => {
147
+ debug('Unsubscribing to remote patches')
148
+ sub.unsubscribe()
149
+ }
150
+ })
156
151
 
157
152
  editor.apply = (operation: Operation): void | Editor => {
158
153
  if (readOnly) {
@@ -20,7 +20,7 @@ import {
20
20
  type Descendant,
21
21
  type SelectionOperation,
22
22
  } from 'slate'
23
- import type {PatchObservable, PortableTextSlateEditor} from '../../types/editor'
23
+ import type {PortableTextSlateEditor} from '../../types/editor'
24
24
  import {debugWithName} from '../../utils/debug'
25
25
  import {fromSlateValue} from '../../utils/values'
26
26
  import {
@@ -29,6 +29,7 @@ import {
29
29
  withRedoing,
30
30
  withUndoing,
31
31
  } from '../../utils/withUndoRedo'
32
+ import type {EditorActor} from '../editor-machine'
32
33
 
33
34
  const debug = debugWithName('plugin:withUndoRedo')
34
35
  const debugVerbose = debug.enabled && false
@@ -51,7 +52,7 @@ const isSaving = (editor: Editor): boolean | undefined => {
51
52
  }
52
53
 
53
54
  export interface Options {
54
- patches$?: PatchObservable
55
+ editorActor: EditorActor
55
56
  readOnly: boolean
56
57
  blockSchemaType: ObjectSchemaType
57
58
  }
@@ -66,7 +67,7 @@ const getRemotePatches = (editor: Editor) => {
66
67
  export function createWithUndoRedo(
67
68
  options: Options,
68
69
  ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
69
- const {readOnly, patches$, blockSchemaType} = options
70
+ const {editorActor, readOnly, blockSchemaType} = options
70
71
 
71
72
  return (editor: PortableTextSlateEditor) => {
72
73
  let previousSnapshot: PortableTextBlock[] | undefined = fromSlateValue(
@@ -74,39 +75,39 @@ export function createWithUndoRedo(
74
75
  blockSchemaType.name,
75
76
  )
76
77
  const remotePatches = getRemotePatches(editor)
77
- if (patches$) {
78
- editor.subscriptions.push(() => {
79
- debug('Subscribing to patches')
80
- const sub = patches$.subscribe(({patches, snapshot}) => {
81
- let reset = false
82
- patches.forEach((patch) => {
83
- if (!reset && patch.origin !== 'local' && remotePatches) {
84
- if (patch.type === 'unset' && patch.path.length === 0) {
85
- debug(
86
- 'Someone else cleared the content, resetting undo/redo history',
87
- )
88
- editor.history = {undos: [], redos: []}
89
- remotePatches.splice(0, remotePatches.length)
90
- SAVING.set(editor, true)
91
- reset = true
92
- return
93
- }
94
- remotePatches.push({
95
- patch,
96
- time: new Date(),
97
- snapshot,
98
- previousSnapshot,
99
- })
78
+
79
+ editor.subscriptions.push(() => {
80
+ debug('Subscribing to patches')
81
+ const sub = editorActor.on('patches', ({patches, snapshot}) => {
82
+ let reset = false
83
+ patches.forEach((patch) => {
84
+ if (!reset && patch.origin !== 'local' && remotePatches) {
85
+ if (patch.type === 'unset' && patch.path.length === 0) {
86
+ debug(
87
+ 'Someone else cleared the content, resetting undo/redo history',
88
+ )
89
+ editor.history = {undos: [], redos: []}
90
+ remotePatches.splice(0, remotePatches.length)
91
+ SAVING.set(editor, true)
92
+ reset = true
93
+ return
100
94
  }
101
- })
102
- previousSnapshot = snapshot
95
+ remotePatches.push({
96
+ patch,
97
+ time: new Date(),
98
+ snapshot,
99
+ previousSnapshot,
100
+ })
101
+ }
103
102
  })
104
- return () => {
105
- debug('Unsubscribing to patches')
106
- sub.unsubscribe()
107
- }
103
+ previousSnapshot = snapshot
108
104
  })
109
- }
105
+ return () => {
106
+ debug('Unsubscribing to patches')
107
+ sub.unsubscribe()
108
+ }
109
+ })
110
+
110
111
  editor.history = {undos: [], redos: []}
111
112
  const {apply} = editor
112
113
  editor.apply = (op: Operation) => {
@@ -47,8 +47,7 @@ export const withPlugins = <T extends Editor>(
47
47
  options: createEditorOptions,
48
48
  ): {editor: PortableTextSlateEditor; subscribe: () => () => void} => {
49
49
  const e = editor as T & PortableTextSlateEditor
50
- const {editorActor, portableTextEditor, patches$, readOnly, maxBlocks} =
51
- options
50
+ const {editorActor, portableTextEditor, readOnly, maxBlocks} = options
52
51
  const {schemaTypes} = portableTextEditor
53
52
  e.subscriptions = []
54
53
  if (e.destroy) {
@@ -75,7 +74,6 @@ export const withPlugins = <T extends Editor>(
75
74
  )
76
75
  const withPatches = createWithPatches({
77
76
  editorActor,
78
- patches$,
79
77
  patchFunctions: operationToPatches,
80
78
  readOnly,
81
79
  schemaTypes,
@@ -83,8 +81,8 @@ export const withPlugins = <T extends Editor>(
83
81
  const withMaxBlocks = createWithMaxBlocks(maxBlocks || -1)
84
82
  const withPortableTextLists = createWithPortableTextLists(schemaTypes)
85
83
  const withUndoRedo = createWithUndoRedo({
84
+ editorActor,
86
85
  readOnly,
87
- patches$,
88
86
  blockSchemaType: schemaTypes.block,
89
87
  })
90
88
  const withPortableTextMarkModel = createWithPortableTextMarkModel(
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export {
15
15
  type EditorActor,
16
16
  type MutationEvent,
17
17
  type PatchEvent,
18
+ type PatchesEvent,
18
19
  } from './editor/editor-machine'
19
20
  export {usePortableTextEditor} from './editor/hooks/usePortableTextEditor'
20
21
  export {usePortableTextEditorSelection} from './editor/hooks/usePortableTextEditorSelection'
@@ -1,14 +1,12 @@
1
1
  import type {BaseSyntheticEvent} from 'react'
2
2
  import type {EditorActor} from '../editor/editor-machine'
3
3
  import type {PortableTextEditor} from '../editor/PortableTextEditor'
4
- import type {PatchObservable} from './editor'
5
4
 
6
5
  /**
7
6
  * @internal
8
7
  */
9
8
  export type createEditorOptions = {
10
9
  editorActor: EditorActor
11
- patches$?: PatchObservable
12
10
  portableTextEditor: PortableTextEditor
13
11
  readOnly: boolean
14
12
  maxBlocks?: number