@portabletext/editor 1.3.0 → 1.4.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.
@@ -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.
@@ -21,6 +21,7 @@ import {toPortableTextRange} from '../utils/ranges'
21
21
  import {fromSlateValue} from '../utils/values'
22
22
  import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
23
23
  import {performAction, performDefaultAction} from './behavior/behavior.actions'
24
+ import {coreBehaviors} from './behavior/behavior.core'
24
25
  import type {
25
26
  Behavior,
26
27
  BehaviorAction,
@@ -56,6 +57,15 @@ const networkLogic = fromCallback(({sendBack}) => {
56
57
  */
57
58
  export type PatchEvent = {type: 'patch'; patch: Patch}
58
59
 
60
+ /**
61
+ * @internal
62
+ */
63
+ export type PatchesEvent = {
64
+ type: 'patches'
65
+ patches: Array<Patch>
66
+ snapshot: Array<PortableTextBlock> | undefined
67
+ }
68
+
59
69
  /**
60
70
  * @internal
61
71
  */
@@ -82,11 +92,16 @@ type EditorEvent =
82
92
  type: 'update schema'
83
93
  schema: PortableTextMemberSchemaTypes
84
94
  }
95
+ | {
96
+ type: 'update behaviors'
97
+ behaviors: Array<Behavior>
98
+ }
85
99
  | EditorEmittedEvent
86
100
 
87
101
  type EditorEmittedEvent =
88
102
  | {type: 'ready'}
89
103
  | PatchEvent
104
+ | PatchesEvent
90
105
  | MutationEvent
91
106
  | {
92
107
  type: 'unset'
@@ -129,12 +144,18 @@ export const editorMachine = setup({
129
144
  events: {} as EditorEvent,
130
145
  emitted: {} as EditorEmittedEvent,
131
146
  input: {} as {
132
- behaviors: Array<Behavior>
147
+ behaviors?: Array<Behavior>
133
148
  keyGenerator: () => string
134
149
  schema: PortableTextMemberSchemaTypes
135
150
  },
136
151
  },
137
152
  actions: {
153
+ 'assign behaviors': assign({
154
+ behaviors: ({event}) => {
155
+ assertEvent(event, 'update behaviors')
156
+ return [...coreBehaviors, ...event.behaviors]
157
+ },
158
+ }),
138
159
  'assign schema': assign({
139
160
  schema: ({event}) => {
140
161
  assertEvent(event, 'update schema')
@@ -253,7 +274,9 @@ export const editorMachine = setup({
253
274
  }).createMachine({
254
275
  id: 'editor',
255
276
  context: ({input}) => ({
256
- behaviors: input.behaviors,
277
+ behaviors: input.behaviors
278
+ ? [...coreBehaviors, ...input.behaviors]
279
+ : coreBehaviors,
257
280
  keyGenerator: input.keyGenerator,
258
281
  pendingEvents: [],
259
282
  schema: input.schema,
@@ -274,7 +297,9 @@ export const editorMachine = setup({
274
297
  'online': {actions: emit({type: 'online'})},
275
298
  'offline': {actions: emit({type: 'offline'})},
276
299
  'loading': {actions: emit({type: 'loading'})},
300
+ 'patches': {actions: emit(({event}) => event)},
277
301
  'done loading': {actions: emit({type: 'done loading'})},
302
+ 'update behaviors': {actions: 'assign behaviors'},
278
303
  'update schema': {actions: 'assign schema'},
279
304
  'behavior event': {actions: 'handle behavior event'},
280
305
  'behavior action intends': {
@@ -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(
@@ -0,0 +1,45 @@
1
+ import type {
2
+ ArrayDefinition,
3
+ ArraySchemaType,
4
+ PortableTextBlock,
5
+ } from '@sanity/types'
6
+ import {useActorRef} from '@xstate/react'
7
+ import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
8
+ import {compileType} from '../utils/schema'
9
+ import type {Behavior} from './behavior/behavior.types'
10
+ import {editorMachine} from './editor-machine'
11
+ import {defaultKeyGenerator} from './key-generator'
12
+
13
+ /**
14
+ * @alpha
15
+ */
16
+ export type EditorConfig = {
17
+ behaviors?: Array<Behavior>
18
+ keyGenerator?: () => string
19
+ schema: ArraySchemaType<PortableTextBlock> | ArrayDefinition
20
+ }
21
+
22
+ /**
23
+ * @alpha
24
+ */
25
+ export type Editor = ReturnType<typeof useEditor>
26
+
27
+ /**
28
+ * @alpha
29
+ */
30
+ export function useEditor(config: EditorConfig) {
31
+ const schema = getPortableTextMemberSchemaTypes(
32
+ config.schema.hasOwnProperty('jsonType')
33
+ ? config.schema
34
+ : compileType(config.schema),
35
+ )
36
+ const editorActor = useActorRef(editorMachine, {
37
+ input: {
38
+ behaviors: config.behaviors,
39
+ keyGenerator: config.keyGenerator ?? defaultKeyGenerator,
40
+ schema,
41
+ },
42
+ })
43
+
44
+ return editorActor
45
+ }
package/src/index.ts CHANGED
@@ -1,13 +1,18 @@
1
1
  export type {Patch} from '@portabletext/patches'
2
- export type {
3
- Behavior,
4
- BehaviorActionIntend,
5
- BehaviorContext,
6
- BehaviorEvent,
7
- BehaviorGuard,
8
- PickFromUnion,
9
- BehaviorActionIntendSet,
2
+ export {
3
+ type Behavior,
4
+ type BehaviorActionIntend,
5
+ type BehaviorContext,
6
+ type BehaviorEvent,
7
+ type BehaviorGuard,
8
+ type PickFromUnion,
9
+ type BehaviorActionIntendSet,
10
+ defineBehavior,
10
11
  } from './editor/behavior/behavior.types'
12
+ export {
13
+ createMarkdownBehaviors,
14
+ type MarkdownBehaviorsConfig,
15
+ } from './editor/behavior/behavior.markdown'
11
16
  export {PortableTextEditable} from './editor/Editable'
12
17
  export type {PortableTextEditableProps} from './editor/Editable'
13
18
  export {
@@ -15,11 +20,13 @@ export {
15
20
  type EditorActor,
16
21
  type MutationEvent,
17
22
  type PatchEvent,
23
+ type PatchesEvent,
18
24
  } from './editor/editor-machine'
19
25
  export {usePortableTextEditor} from './editor/hooks/usePortableTextEditor'
20
26
  export {usePortableTextEditorSelection} from './editor/hooks/usePortableTextEditorSelection'
21
27
  export {defaultKeyGenerator as keyGenerator} from './editor/key-generator'
22
28
  export {PortableTextEditor} from './editor/PortableTextEditor'
23
29
  export type {PortableTextEditorProps} from './editor/PortableTextEditor'
30
+ export {useEditor, type Editor, type EditorConfig} from './editor/use-editor'
24
31
  export * from './types/editor'
25
32
  export * from './types/options'
@@ -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