@portabletext/editor 1.6.0 → 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 (37) 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 +4337 -4089
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +4325 -4077
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +4337 -4089
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +10 -11
  11. package/src/editor/Editable.tsx +26 -28
  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/editor/withSyncRangeDecorations.ts +20 -0
  31. package/src/index.ts +9 -1
  32. package/src/types/editor.ts +0 -1
  33. package/src/types/options.ts +1 -3
  34. package/src/utils/__tests__/operationToPatches.test.ts +7 -14
  35. package/src/utils/__tests__/patchToOperations.test.ts +4 -7
  36. package/src/editor/components/SlateContainer.tsx +0 -79
  37. package/src/editor/hooks/usePortableTextReadOnly.ts +0 -20
@@ -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
+ }
@@ -28,8 +28,11 @@ import type {
28
28
  BehaviorActionIntend,
29
29
  BehaviorContext,
30
30
  BehaviorEvent,
31
+ PickFromUnion,
31
32
  } from './behavior/behavior.types'
32
33
 
34
+ export * from 'xstate/guards'
35
+
33
36
  /**
34
37
  * @internal
35
38
  */
@@ -75,7 +78,10 @@ export type MutationEvent = {
75
78
  snapshot: Array<PortableTextBlock> | undefined
76
79
  }
77
80
 
78
- type EditorEvent =
81
+ /**
82
+ * @internal
83
+ */
84
+ export type InternalEditorEvent =
79
85
  | {type: 'normalizing'}
80
86
  | {type: 'done normalizing'}
81
87
  | {
@@ -96,9 +102,19 @@ type EditorEvent =
96
102
  type: 'update behaviors'
97
103
  behaviors: Array<Behavior>
98
104
  }
99
- | EditorEmittedEvent
105
+ | {
106
+ type: 'toggle readOnly'
107
+ }
108
+ | {
109
+ type: 'update maxBlocks'
110
+ maxBlocks: number | undefined
111
+ }
112
+ | InternalEditorEmittedEvent
100
113
 
101
- type EditorEmittedEvent =
114
+ /**
115
+ * @internal
116
+ */
117
+ export type InternalEditorEmittedEvent =
102
118
  | {type: 'ready'}
103
119
  | PatchEvent
104
120
  | PatchesEvent
@@ -124,11 +140,16 @@ type EditorEmittedEvent =
124
140
  }
125
141
  | {type: 'selection'; selection: EditorSelection}
126
142
  | {type: 'blur'; event: FocusEvent<HTMLDivElement, Element>}
127
- | {type: 'focus'; event: FocusEvent<HTMLDivElement, Element>}
143
+ | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
128
144
  | {type: 'online'}
129
145
  | {type: 'offline'}
130
146
  | {type: 'loading'}
131
147
  | {type: 'done loading'}
148
+ | PickFromUnion<
149
+ BehaviorEvent,
150
+ 'type',
151
+ 'annotation.add' | 'annotation.remove' | 'annotation.toggle' | 'focus'
152
+ >
132
153
 
133
154
  /**
134
155
  * @internal
@@ -140,9 +161,11 @@ export const editorMachine = setup({
140
161
  keyGenerator: () => string
141
162
  pendingEvents: Array<PatchEvent | MutationEvent>
142
163
  schema: PortableTextMemberSchemaTypes
164
+ readOnly: boolean
165
+ maxBlocks: number | undefined
143
166
  },
144
- events: {} as EditorEvent,
145
- emitted: {} as EditorEmittedEvent,
167
+ events: {} as InternalEditorEvent,
168
+ emitted: {} as InternalEditorEmittedEvent,
146
169
  input: {} as {
147
170
  behaviors?: Array<Behavior>
148
171
  keyGenerator: () => string
@@ -290,12 +313,30 @@ export const editorMachine = setup({
290
313
  keyGenerator: input.keyGenerator,
291
314
  pendingEvents: [],
292
315
  schema: input.schema,
316
+ readOnly: false,
317
+ maxBlocks: undefined,
293
318
  }),
294
319
  invoke: {
295
320
  id: 'networkLogic',
296
321
  src: 'networkLogic',
297
322
  },
298
323
  on: {
324
+ 'annotation.add': {
325
+ actions: emit(({event}) => event),
326
+ guard: ({context}) => !context.readOnly,
327
+ },
328
+ 'annotation.remove': {
329
+ actions: emit(({event}) => event),
330
+ guard: ({context}) => !context.readOnly,
331
+ },
332
+ 'annotation.toggle': {
333
+ actions: emit(({event}) => event),
334
+ guard: ({context}) => !context.readOnly,
335
+ },
336
+ 'focus': {
337
+ actions: emit(({event}) => event),
338
+ guard: ({context}) => !context.readOnly,
339
+ },
299
340
  'ready': {actions: emit(({event}) => event)},
300
341
  'unset': {actions: emit(({event}) => event)},
301
342
  'value changed': {actions: emit(({event}) => event)},
@@ -303,7 +344,7 @@ export const editorMachine = setup({
303
344
  'error': {actions: emit(({event}) => event)},
304
345
  'selection': {actions: emit(({event}) => event)},
305
346
  'blur': {actions: emit(({event}) => event)},
306
- 'focus': {actions: emit(({event}) => event)},
347
+ 'focused': {actions: emit(({event}) => event)},
307
348
  'online': {actions: emit({type: 'online'})},
308
349
  'offline': {actions: emit({type: 'offline'})},
309
350
  'loading': {actions: emit({type: 'loading'})},
@@ -311,7 +352,16 @@ export const editorMachine = setup({
311
352
  'done loading': {actions: emit({type: 'done loading'})},
312
353
  'update behaviors': {actions: 'assign behaviors'},
313
354
  'update schema': {actions: 'assign schema'},
314
- 'behavior event': {actions: 'handle behavior event'},
355
+ 'toggle readOnly': {
356
+ actions: assign({readOnly: ({context}) => !context.readOnly}),
357
+ },
358
+ 'update maxBlocks': {
359
+ actions: assign({maxBlocks: ({event}) => event.maxBlocks}),
360
+ },
361
+ 'behavior event': {
362
+ actions: 'handle behavior event',
363
+ guard: ({context}) => !context.readOnly,
364
+ },
315
365
  'behavior action intends': {
316
366
  actions: [
317
367
  ({context, event}) => {
@@ -1,6 +1,35 @@
1
- import {randomKey} from '@sanity/util/content'
1
+ import getRandomValues from 'get-random-values-esm'
2
2
 
3
3
  /**
4
4
  * @public
5
5
  */
6
6
  export const defaultKeyGenerator = (): string => randomKey(12)
7
+
8
+ const getByteHexTable = (() => {
9
+ let table: any[]
10
+ return () => {
11
+ if (table) {
12
+ return table
13
+ }
14
+
15
+ table = []
16
+ for (let i = 0; i < 256; ++i) {
17
+ table[i] = (i + 0x100).toString(16).slice(1)
18
+ }
19
+ return table
20
+ }
21
+ })()
22
+
23
+ // WHATWG crypto RNG - https://w3c.github.io/webcrypto/Overview.html
24
+ function whatwgRNG(length = 16) {
25
+ const rnds8 = new Uint8Array(length)
26
+ getRandomValues(rnds8)
27
+ return rnds8
28
+ }
29
+
30
+ function randomKey(length?: number): string {
31
+ const table = getByteHexTable()
32
+ return whatwgRNG(length)
33
+ .reduce((str, n) => str + table[n], '')
34
+ .slice(0, length)
35
+ }
@@ -1,8 +1,69 @@
1
1
  import type {Editor} from 'slate'
2
2
  import type {EditorActor} from '../editor-machine'
3
3
 
4
- export function createWithEventListeners(editorActor: EditorActor) {
4
+ export function createWithEventListeners(
5
+ editorActor: EditorActor,
6
+ subscriptions: Array<() => () => void>,
7
+ ) {
5
8
  return function withEventListeners(editor: Editor) {
9
+ if (editorActor.getSnapshot().context.maxBlocks !== undefined) {
10
+ return editor
11
+ }
12
+
13
+ subscriptions.push(() => {
14
+ const subscription = editorActor.on('*', (event) => {
15
+ switch (event.type) {
16
+ case 'annotation.add': {
17
+ editorActor.send({
18
+ type: 'behavior event',
19
+ behaviorEvent: {
20
+ type: 'annotation.add',
21
+ annotation: event.annotation,
22
+ },
23
+ editor,
24
+ })
25
+ break
26
+ }
27
+ case 'annotation.remove': {
28
+ editorActor.send({
29
+ type: 'behavior event',
30
+ behaviorEvent: {
31
+ type: 'annotation.remove',
32
+ annotation: event.annotation,
33
+ },
34
+ editor,
35
+ })
36
+ break
37
+ }
38
+ case 'annotation.toggle': {
39
+ editorActor.send({
40
+ type: 'behavior event',
41
+ behaviorEvent: {
42
+ type: 'annotation.toggle',
43
+ annotation: event.annotation,
44
+ },
45
+ editor,
46
+ })
47
+ break
48
+ }
49
+ case 'focus': {
50
+ editorActor.send({
51
+ type: 'behavior event',
52
+ behaviorEvent: {
53
+ type: 'focus',
54
+ },
55
+ editor,
56
+ })
57
+ break
58
+ }
59
+ }
60
+ })
61
+
62
+ return () => {
63
+ subscription.unsubscribe()
64
+ }
65
+ })
66
+
6
67
  editor.addMark = (mark) => {
7
68
  editorActor.send({
8
69
  type: 'behavior event',