@portabletext/editor 1.10.1 → 1.11.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.
@@ -7,7 +7,6 @@ import {
7
7
  assign,
8
8
  emit,
9
9
  enqueueActions,
10
- fromCallback,
11
10
  setup,
12
11
  type ActorRefFrom,
13
12
  } from 'xstate'
@@ -28,6 +27,7 @@ import type {
28
27
  BehaviorActionIntend,
29
28
  BehaviorContext,
30
29
  BehaviorEvent,
30
+ OmitFromUnion,
31
31
  PickFromUnion,
32
32
  } from './behavior/behavior.types'
33
33
 
@@ -38,27 +38,6 @@ export * from 'xstate/guards'
38
38
  */
39
39
  export type EditorActor = ActorRefFrom<typeof editorMachine>
40
40
 
41
- const networkLogic = fromCallback(({sendBack}) => {
42
- const onlineHandler = () => {
43
- sendBack({type: 'online'})
44
- }
45
- const offlineHandler = () => {
46
- sendBack({type: 'offline'})
47
- }
48
-
49
- if (window) {
50
- window.addEventListener('online', onlineHandler)
51
- window.addEventListener('offline', offlineHandler)
52
- }
53
-
54
- return () => {
55
- if (window) {
56
- window.removeEventListener('online', onlineHandler)
57
- window.removeEventListener('offline', offlineHandler)
58
- }
59
- }
60
- })
61
-
62
41
  /**
63
42
  * @internal
64
43
  */
@@ -117,7 +96,27 @@ export type InternalEditorEvent =
117
96
  type: 'update maxBlocks'
118
97
  maxBlocks: number | undefined
119
98
  }
120
- | InternalEditorEmittedEvent
99
+ | OmitFromUnion<InternalEditorEmittedEvent, 'type', 'readOnly toggled'>
100
+
101
+ /**
102
+ * @alpha
103
+ */
104
+ export type EditorEmittedEvent = PickFromUnion<
105
+ InternalEditorEmittedEvent,
106
+ 'type',
107
+ | 'blur'
108
+ | 'done loading'
109
+ | 'error'
110
+ | 'focus'
111
+ | 'invalid value'
112
+ | 'loading'
113
+ | 'mutation'
114
+ | 'patch'
115
+ | 'readOnly toggled'
116
+ | 'ready'
117
+ | 'selection'
118
+ | 'value changed'
119
+ >
121
120
 
122
121
  /**
123
122
  * @internal
@@ -149,10 +148,9 @@ export type InternalEditorEmittedEvent =
149
148
  | {type: 'selection'; selection: EditorSelection}
150
149
  | {type: 'blur'; event: FocusEvent<HTMLDivElement, Element>}
151
150
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
152
- | {type: 'online'}
153
- | {type: 'offline'}
154
151
  | {type: 'loading'}
155
152
  | {type: 'done loading'}
153
+ | {type: 'readOnly toggled'; readOnly: boolean}
156
154
  | PickFromUnion<
157
155
  BehaviorEvent,
158
156
  'type',
@@ -178,6 +176,8 @@ export const editorMachine = setup({
178
176
  input: {} as {
179
177
  behaviors?: Array<Behavior>
180
178
  keyGenerator: () => string
179
+ maxBlocks?: number
180
+ readOnly?: boolean
181
181
  schema: PortableTextMemberSchemaTypes
182
182
  value?: Array<PortableTextBlock>
183
183
  },
@@ -313,9 +313,6 @@ export const editorMachine = setup({
313
313
  }
314
314
  }),
315
315
  },
316
- actors: {
317
- networkLogic,
318
- },
319
316
  }).createMachine({
320
317
  id: 'editor',
321
318
  context: ({input}) => ({
@@ -323,14 +320,10 @@ export const editorMachine = setup({
323
320
  keyGenerator: input.keyGenerator,
324
321
  pendingEvents: [],
325
322
  schema: input.schema,
326
- readOnly: false,
327
- maxBlocks: undefined,
323
+ readOnly: input.readOnly ?? false,
324
+ maxBlocks: input.maxBlocks,
328
325
  value: input.value,
329
326
  }),
330
- invoke: {
331
- id: 'networkLogic',
332
- src: 'networkLogic',
333
- },
334
327
  on: {
335
328
  'annotation.add': {
336
329
  actions: emit(({event}) => event),
@@ -356,8 +349,6 @@ export const editorMachine = setup({
356
349
  'selection': {actions: emit(({event}) => event)},
357
350
  'blur': {actions: emit(({event}) => event)},
358
351
  'focused': {actions: emit(({event}) => event)},
359
- 'online': {actions: emit({type: 'online'})},
360
- 'offline': {actions: emit({type: 'offline'})},
361
352
  'loading': {actions: emit({type: 'loading'})},
362
353
  'patches': {actions: emit(({event}) => event)},
363
354
  'done loading': {actions: emit({type: 'done loading'})},
@@ -365,7 +356,13 @@ export const editorMachine = setup({
365
356
  'update schema': {actions: 'assign schema'},
366
357
  'update value': {actions: assign({value: ({event}) => event.value})},
367
358
  'toggle readOnly': {
368
- actions: assign({readOnly: ({context}) => !context.readOnly}),
359
+ actions: [
360
+ assign({readOnly: ({context}) => !context.readOnly}),
361
+ emit(({context}) => ({
362
+ type: 'readOnly toggled',
363
+ readOnly: context.readOnly,
364
+ })),
365
+ ],
369
366
  },
370
367
  'update maxBlocks': {
371
368
  actions: assign({maxBlocks: ({event}) => event.maxBlocks}),
@@ -0,0 +1,81 @@
1
+ import React, {useMemo} from 'react'
2
+ import {Slate} from 'slate-react'
3
+ import {Synchronizer} from './components/Synchronizer'
4
+ import {EditorActorContext} from './editor-actor-context'
5
+ import {PortableTextEditorContext} from './hooks/usePortableTextEditor'
6
+ import {PortableTextEditorSelectionProvider} from './hooks/usePortableTextEditorSelection'
7
+ import {
8
+ PortableTextEditor,
9
+ RouteEventsToChanges,
10
+ type PortableTextEditorProps,
11
+ } from './PortableTextEditor'
12
+ import {useEditor, type Editor, type EditorConfig} from './use-editor'
13
+
14
+ const EditorContext = React.createContext<Editor | undefined>(undefined)
15
+
16
+ /**
17
+ * @alpha
18
+ */
19
+ export type EditorProviderProps = {
20
+ config: EditorConfig
21
+ children?: React.ReactNode
22
+ }
23
+
24
+ /**
25
+ * @alpha
26
+ */
27
+ export function EditorProvider(props: EditorProviderProps) {
28
+ const editor = useEditor(props.config)
29
+ const editorActor = editor._internal.editorActor
30
+ const slateEditor = editor._internal.slateEditor
31
+ const editable = editor.editable
32
+ const portableTextEditor = useMemo(
33
+ () =>
34
+ new PortableTextEditor({
35
+ editor,
36
+ } as unknown as PortableTextEditorProps),
37
+ [editor],
38
+ )
39
+
40
+ return (
41
+ <EditorContext.Provider value={editor}>
42
+ <RouteEventsToChanges
43
+ editorActor={editorActor}
44
+ onChange={(change) => {
45
+ portableTextEditor.change$.next(change)
46
+ }}
47
+ />
48
+ <Synchronizer
49
+ editorActor={editorActor}
50
+ getValue={editable.getValue}
51
+ portableTextEditor={portableTextEditor}
52
+ slateEditor={slateEditor.instance}
53
+ />
54
+ <EditorActorContext.Provider value={editorActor}>
55
+ <Slate
56
+ editor={slateEditor.instance}
57
+ initialValue={slateEditor.initialValue}
58
+ >
59
+ <PortableTextEditorContext.Provider value={portableTextEditor}>
60
+ <PortableTextEditorSelectionProvider editorActor={editorActor}>
61
+ {props.children}
62
+ </PortableTextEditorSelectionProvider>
63
+ </PortableTextEditorContext.Provider>
64
+ </Slate>
65
+ </EditorActorContext.Provider>
66
+ </EditorContext.Provider>
67
+ )
68
+ }
69
+
70
+ /**
71
+ * @alpha
72
+ */
73
+ export function useEditorContext() {
74
+ const editor = React.useContext(EditorContext)
75
+
76
+ if (!editor) {
77
+ throw new Error('No Editor set. Use EditorProvider to set one.')
78
+ }
79
+
80
+ return editor
81
+ }
@@ -2,7 +2,6 @@ import type {PortableTextBlock} from '@sanity/types'
2
2
  import {debounce, isEqual} from 'lodash'
3
3
  import {useCallback, useMemo, useRef} from 'react'
4
4
  import {Editor, Text, Transforms, type Descendant, type Node} from 'slate'
5
- import {useSlate} from 'slate-react'
6
5
  import type {PortableTextSlateEditor} from '../../types/editor'
7
6
  import {debugWithName} from '../../utils/debug'
8
7
  import {validateValue} from '../../utils/validateValue'
@@ -26,6 +25,7 @@ export interface UseSyncValueProps {
26
25
  editorActor: EditorActor
27
26
  portableTextEditor: PortableTextEditor
28
27
  readOnly: boolean
28
+ slateEditor: PortableTextSlateEditor
29
29
  }
30
30
 
31
31
  const CURRENT_VALUE = new WeakMap<
@@ -51,10 +51,9 @@ export function useSyncValue(
51
51
  value: PortableTextBlock[] | undefined,
52
52
  userCallbackFn?: () => void,
53
53
  ) => void {
54
- const {editorActor, portableTextEditor, readOnly} = props
54
+ const {editorActor, portableTextEditor, readOnly, slateEditor} = props
55
55
  const schemaTypes = editorActor.getSnapshot().context.schema
56
56
  const previousValue = useRef<PortableTextBlock[] | undefined>()
57
- const slateEditor = useSlate()
58
57
  const updateValueFunctionRef =
59
58
  useRef<(value: PortableTextBlock[] | undefined) => void>()
60
59
 
@@ -21,11 +21,6 @@ export interface OriginalEditorFunctions {
21
21
  normalizeNode: (entry: NodeEntry<Node>) => void
22
22
  }
23
23
 
24
- const originalFnMap = new WeakMap<
25
- PortableTextSlateEditor,
26
- OriginalEditorFunctions
27
- >()
28
-
29
24
  type PluginsOptions = {
30
25
  editorActor: EditorActor
31
26
  subscriptions: Array<() => () => void>
@@ -38,17 +33,6 @@ export const withPlugins = <T extends Editor>(
38
33
  const e = editor as T & PortableTextSlateEditor
39
34
  const {editorActor} = options
40
35
  const schemaTypes = editorActor.getSnapshot().context.schema
41
- if (e.destroy) {
42
- e.destroy()
43
- } else {
44
- // Save a copy of the original editor functions here before they were changed by plugins.
45
- // We will put them back when .destroy is called (see below).
46
- originalFnMap.set(e, {
47
- apply: e.apply,
48
- onChange: e.onChange,
49
- normalizeNode: e.normalizeNode,
50
- })
51
- }
52
36
  const operationToPatches = createOperationToPatches(schemaTypes)
53
37
  const withObjectKeys = createWithObjectKeys(editorActor, schemaTypes)
54
38
  const withSchemaTypes = createWithSchemaTypes({
@@ -92,17 +76,6 @@ export const withPlugins = <T extends Editor>(
92
76
  options.subscriptions,
93
77
  )
94
78
 
95
- e.destroy = () => {
96
- const originalFunctions = originalFnMap.get(e)
97
- if (!originalFunctions) {
98
- throw new Error('Could not find pristine versions of editor functions')
99
- }
100
- e.apply = originalFunctions.apply
101
- e.history = {undos: [], redos: []}
102
- e.normalizeNode = originalFunctions.normalizeNode
103
- e.onChange = originalFunctions.onChange
104
- }
105
-
106
79
  // Ordering is important here, selection dealing last, data manipulation in the middle and core model stuff first.
107
80
  return withEventListeners(
108
81
  withSchemaTypes(
@@ -3,7 +3,15 @@ import type {
3
3
  ArraySchemaType,
4
4
  PortableTextBlock,
5
5
  } from '@sanity/types'
6
- import {useActorRef, useSelector} from '@xstate/react'
6
+ import {useActorRef} from '@xstate/react'
7
+ import {useCallback, useMemo} from 'react'
8
+ import {
9
+ createActor,
10
+ type ActorRef,
11
+ type EventObject,
12
+ type Snapshot,
13
+ } from 'xstate'
14
+ import type {EditableAPI} from '../types/editor'
7
15
  import {getPortableTextMemberSchemaTypes} from '../utils/getPortableTextMemberSchemaTypes'
8
16
  import {compileType} from '../utils/schema'
9
17
  import type {Behavior, PickFromUnion} from './behavior/behavior.types'
@@ -12,9 +20,11 @@ import {compileSchemaDefinition, type SchemaDefinition} from './define-schema'
12
20
  import {
13
21
  editorMachine,
14
22
  type EditorActor,
23
+ type EditorEmittedEvent,
15
24
  type InternalEditorEvent,
16
25
  } from './editor-machine'
17
26
  import {defaultKeyGenerator} from './key-generator'
27
+ import {createEditableAPI} from './plugins/createWithEditableAPI'
18
28
 
19
29
  /**
20
30
  * @alpha
@@ -22,6 +32,8 @@ import {defaultKeyGenerator} from './key-generator'
22
32
  export type EditorConfig = {
23
33
  behaviors?: Array<Behavior>
24
34
  keyGenerator?: () => string
35
+ maxBlocks?: number
36
+ readOnly?: boolean
25
37
  initialValue?: Array<PortableTextBlock>
26
38
  } & (
27
39
  | {
@@ -53,8 +65,8 @@ export type EditorEvent = PickFromUnion<
53
65
  */
54
66
  export type Editor = {
55
67
  send: (event: EditorEvent) => void
56
- on: EditorActor['on']
57
- readOnly: boolean
68
+ on: ActorRef<Snapshot<unknown>, EventObject, EditorEmittedEvent>['on']
69
+ editable: EditableAPI
58
70
  _internal: {
59
71
  editorActor: EditorActor
60
72
  slateEditor: SlateEditor
@@ -64,33 +76,90 @@ export type Editor = {
64
76
  /**
65
77
  * @alpha
66
78
  */
67
- export function useEditor(config: EditorConfig): Editor {
68
- const editorActor = useActorRef(editorMachine, {
69
- input: {
70
- behaviors: config.behaviors,
71
- keyGenerator: config.keyGenerator ?? defaultKeyGenerator,
72
- schema: config.schemaDefinition
73
- ? compileSchemaDefinition(config.schemaDefinition)
74
- : getPortableTextMemberSchemaTypes(
75
- config.schema.hasOwnProperty('jsonType')
76
- ? config.schema
77
- : compileType(config.schema),
78
- ),
79
- value: config.initialValue,
80
- },
79
+ export function createEditor(config: EditorConfig): Editor {
80
+ const editorActor = createActor(editorMachine, {
81
+ input: editorConfigToMachineInput(config),
81
82
  })
83
+
84
+ editorActor.start()
85
+
82
86
  const slateEditor = createSlateEditor({editorActor})
83
- const readOnly = useSelector(editorActor, (s) => s.context.readOnly)
87
+ const editable = createEditableAPI(slateEditor.instance, editorActor)
84
88
 
85
89
  return {
86
90
  send: (event) => {
87
91
  editorActor.send(event)
88
92
  },
89
- on: (event, listener) => editorActor.on(event, listener),
90
- readOnly,
93
+ on: (event, listener) =>
94
+ editorActor.on(
95
+ event,
96
+ // @ts-ignore
97
+ listener,
98
+ ),
99
+ editable,
91
100
  _internal: {
92
101
  editorActor,
93
102
  slateEditor,
94
103
  },
95
104
  }
96
105
  }
106
+
107
+ /**
108
+ * @alpha
109
+ */
110
+ export function useEditor(config: EditorConfig): Editor {
111
+ const editorActor = useActorRef(editorMachine, {
112
+ input: editorConfigToMachineInput(config),
113
+ })
114
+ const slateEditor = createSlateEditor({editorActor})
115
+ const editable = useMemo(
116
+ () => createEditableAPI(slateEditor.instance, editorActor),
117
+ [slateEditor.instance, editorActor],
118
+ )
119
+ const send = useCallback(
120
+ (event: EditorEvent) => {
121
+ editorActor.send(event)
122
+ },
123
+ [editorActor],
124
+ )
125
+ const on = useCallback<Editor['on']>(
126
+ (event, listener) =>
127
+ editorActor.on(
128
+ event,
129
+ // @ts-ignore
130
+ listener,
131
+ ),
132
+ [editorActor],
133
+ )
134
+ const editor: Editor = useMemo(
135
+ () => ({
136
+ send,
137
+ on,
138
+ editable,
139
+ _internal: {
140
+ editorActor,
141
+ slateEditor,
142
+ },
143
+ }),
144
+ [send, on, editable, editorActor, slateEditor],
145
+ )
146
+
147
+ return editor
148
+ }
149
+
150
+ function editorConfigToMachineInput(config: EditorConfig) {
151
+ return {
152
+ behaviors: config.behaviors,
153
+ keyGenerator: config.keyGenerator ?? defaultKeyGenerator,
154
+ maxBlocks: config.maxBlocks,
155
+ readOnly: config.readOnly,
156
+ schema: config.schemaDefinition
157
+ ? compileSchemaDefinition(config.schemaDefinition)
158
+ : getPortableTextMemberSchemaTypes(
159
+ config.schema.hasOwnProperty('jsonType')
160
+ ? config.schema
161
+ : compileType(config.schema),
162
+ ),
163
+ value: config.initialValue,
164
+ } as const
165
+ }
package/src/index.ts CHANGED
@@ -1,14 +1,14 @@
1
1
  export type {Patch} from '@portabletext/patches'
2
2
  export type {PortableTextBlock, PortableTextChild} from '@sanity/types'
3
3
  export {coreBehavior, coreBehaviors} from './editor/behavior/behavior.core'
4
- export {
5
- createMarkdownBehaviors,
6
- type MarkdownBehaviorsConfig,
7
- } from './editor/behavior/behavior.markdown'
8
4
  export {
9
5
  createLinkBehaviors,
10
6
  type LinkBehaviorsConfig,
11
7
  } from './editor/behavior/behavior.links'
8
+ export {
9
+ createMarkdownBehaviors,
10
+ type MarkdownBehaviorsConfig,
11
+ } from './editor/behavior/behavior.markdown'
12
12
  export {
13
13
  defineBehavior,
14
14
  type Behavior,
@@ -17,6 +17,7 @@ export {
17
17
  type BehaviorContext,
18
18
  type BehaviorEvent,
19
19
  type BehaviorGuard,
20
+ type OmitFromUnion,
20
21
  type PickFromUnion,
21
22
  } from './editor/behavior/behavior.types'
22
23
  export type {SlateEditor} from './editor/create-slate-editor'
@@ -27,15 +28,22 @@ export {
27
28
  } from './editor/define-schema'
28
29
  export {PortableTextEditable} from './editor/Editable'
29
30
  export type {PortableTextEditableProps} from './editor/Editable'
31
+ export {EditorEventListener} from './editor/editor-event-listener'
30
32
  export {
31
33
  editorMachine,
32
34
  type EditorActor,
35
+ type EditorEmittedEvent,
33
36
  type InternalEditorEmittedEvent,
34
37
  type InternalEditorEvent,
35
38
  type MutationEvent,
36
39
  type PatchEvent,
37
40
  type PatchesEvent,
38
41
  } from './editor/editor-machine'
42
+ export {
43
+ EditorProvider,
44
+ useEditorContext,
45
+ type EditorProviderProps,
46
+ } from './editor/editor-provider'
39
47
  export {usePortableTextEditor} from './editor/hooks/usePortableTextEditor'
40
48
  export {usePortableTextEditorSelection} from './editor/hooks/usePortableTextEditorSelection'
41
49
  export {defaultKeyGenerator as keyGenerator} from './editor/key-generator'
@@ -124,7 +124,6 @@ export type EditorSelection = {
124
124
  export interface PortableTextSlateEditor extends ReactEditor {
125
125
  _key: 'editor'
126
126
  _type: 'editor'
127
- destroy: () => void
128
127
  createPlaceholderBlock: () => Descendant
129
128
  editable: EditableAPI
130
129
  history: History