@portabletext/editor 1.1.4 → 1.1.6

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 (38) hide show
  1. package/README.md +4 -0
  2. package/lib/index.d.mts +631 -30
  3. package/lib/index.d.ts +631 -30
  4. package/lib/index.esm.js +303 -200
  5. package/lib/index.esm.js.map +1 -1
  6. package/lib/index.js +295 -192
  7. package/lib/index.js.map +1 -1
  8. package/lib/index.mjs +303 -200
  9. package/lib/index.mjs.map +1 -1
  10. package/package.json +30 -25
  11. package/src/editor/Editable.tsx +11 -11
  12. package/src/editor/PortableTextEditor.tsx +37 -32
  13. package/src/editor/__tests__/self-solving.test.tsx +1 -1
  14. package/src/editor/behavior/behavior.actions.ts +39 -0
  15. package/src/editor/behavior/behavior.core.ts +37 -0
  16. package/src/editor/behavior/behavior.types.ts +106 -0
  17. package/src/editor/behavior/behavior.utils.ts +34 -0
  18. package/src/editor/components/SlateContainer.tsx +2 -13
  19. package/src/editor/components/Synchronizer.tsx +0 -3
  20. package/src/editor/editor-machine.ts +120 -3
  21. package/src/editor/hooks/useSyncValue.ts +3 -5
  22. package/src/editor/key-generator.ts +6 -0
  23. package/src/editor/plugins/createWithEditableAPI.ts +8 -5
  24. package/src/editor/plugins/createWithHotKeys.ts +1 -32
  25. package/src/editor/plugins/createWithInsertBreak.ts +6 -2
  26. package/src/editor/plugins/createWithInsertData.ts +7 -4
  27. package/src/editor/plugins/createWithObjectKeys.ts +12 -5
  28. package/src/editor/plugins/createWithPatches.ts +0 -1
  29. package/src/editor/plugins/createWithPortableTextMarkModel.ts +85 -114
  30. package/src/editor/plugins/createWithSchemaTypes.ts +3 -4
  31. package/src/editor/plugins/createWithUtils.ts +5 -4
  32. package/src/editor/plugins/index.ts +5 -13
  33. package/src/index.ts +11 -2
  34. package/src/types/options.ts +0 -1
  35. package/src/utils/__tests__/operationToPatches.test.ts +0 -2
  36. package/src/utils/__tests__/patchToOperations.test.ts +1 -2
  37. package/src/utils/sibling-utils.ts +55 -0
  38. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +0 -27
@@ -10,7 +10,21 @@ import {
10
10
  setup,
11
11
  type ActorRefFrom,
12
12
  } from 'xstate'
13
- import type {EditorSelection, InvalidValueResolution} from '../types/editor'
13
+ import type {
14
+ EditorSelection,
15
+ InvalidValueResolution,
16
+ PortableTextMemberSchemaTypes,
17
+ } from '../types/editor'
18
+ import {toPortableTextRange} from '../utils/ranges'
19
+ import {fromSlateValue} from '../utils/values'
20
+ import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
21
+ import {inserText, inserTextBlock} from './behavior/behavior.actions'
22
+ import type {
23
+ Behavior,
24
+ BehaviorAction,
25
+ BehaviorContext,
26
+ BehaviorEvent,
27
+ } from './behavior/behavior.types'
14
28
 
15
29
  /**
16
30
  * @internal
@@ -51,6 +65,12 @@ export type MutationEvent = {
51
65
  type EditorEvent =
52
66
  | {type: 'normalizing'}
53
67
  | {type: 'done normalizing'}
68
+ | BehaviorEvent
69
+ | BehaviorAction
70
+ | {
71
+ type: 'update schema'
72
+ schema: PortableTextMemberSchemaTypes
73
+ }
54
74
  | EditorEmittedEvent
55
75
 
56
76
  type EditorEmittedEvent =
@@ -90,12 +110,34 @@ type EditorEmittedEvent =
90
110
  export const editorMachine = setup({
91
111
  types: {
92
112
  context: {} as {
113
+ behaviors: Array<Behavior>
114
+ keyGenerator: () => string
93
115
  pendingEvents: Array<PatchEvent | MutationEvent>
116
+ schema: PortableTextMemberSchemaTypes
94
117
  },
95
118
  events: {} as EditorEvent,
96
119
  emitted: {} as EditorEmittedEvent,
120
+ input: {} as {
121
+ behaviors: Array<Behavior>
122
+ keyGenerator: () => string
123
+ schema: PortableTextMemberSchemaTypes
124
+ },
97
125
  },
98
126
  actions: {
127
+ 'apply:insert text': ({context, event}) => {
128
+ assertEvent(event, 'insert text')
129
+ inserText({context, event})
130
+ },
131
+ 'apply:insert text block': ({context, event}) => {
132
+ assertEvent(event, 'insert text block')
133
+ inserTextBlock({context, event})
134
+ },
135
+ 'assign schema': assign({
136
+ schema: ({event}) => {
137
+ assertEvent(event, 'update schema')
138
+ return event.schema
139
+ },
140
+ }),
99
141
  'emit patch event': emit(({event}) => {
100
142
  assertEvent(event, 'patch')
101
143
  return event
@@ -118,15 +160,80 @@ export const editorMachine = setup({
118
160
  'clear pending events': assign({
119
161
  pendingEvents: [],
120
162
  }),
163
+ 'handle behavior event': enqueueActions(({context, event, enqueue}) => {
164
+ assertEvent(event, ['key down'])
165
+
166
+ const eventBehaviors = context.behaviors.filter(
167
+ (behavior) => behavior.on === event.type,
168
+ )
169
+
170
+ if (eventBehaviors.length === 0) {
171
+ return
172
+ }
173
+
174
+ const value = fromSlateValue(
175
+ event.editor.children,
176
+ context.schema.block.name,
177
+ KEY_TO_VALUE_ELEMENT.get(event.editor),
178
+ )
179
+ const selection = toPortableTextRange(
180
+ value,
181
+ event.editor.selection,
182
+ context.schema,
183
+ )
184
+
185
+ if (!selection) {
186
+ console.warn(
187
+ `Unable to handle event ${event.type} due to missing selection`,
188
+ )
189
+ return
190
+ }
191
+
192
+ const behaviorContext = {
193
+ schema: context.schema,
194
+ value,
195
+ selection,
196
+ } satisfies BehaviorContext
197
+
198
+ for (const eventBehavior of eventBehaviors) {
199
+ const shouldRun =
200
+ eventBehavior.guard?.({
201
+ context: behaviorContext,
202
+ event,
203
+ }) ?? true
204
+
205
+ if (!shouldRun) {
206
+ continue
207
+ }
208
+
209
+ const actions = eventBehavior.actions.map((action) =>
210
+ action({context: behaviorContext, event}, shouldRun),
211
+ )
212
+
213
+ for (const action of actions) {
214
+ if (typeof action !== 'object') {
215
+ continue
216
+ }
217
+
218
+ enqueue.raise({
219
+ ...action,
220
+ editor: event.editor,
221
+ })
222
+ }
223
+ }
224
+ }),
121
225
  },
122
226
  actors: {
123
227
  networkLogic,
124
228
  },
125
229
  }).createMachine({
126
230
  id: 'editor',
127
- context: {
231
+ context: ({input}) => ({
232
+ behaviors: input.behaviors,
233
+ keyGenerator: input.keyGenerator,
128
234
  pendingEvents: [],
129
- },
235
+ schema: input.schema,
236
+ }),
130
237
  invoke: {
131
238
  id: 'networkLogic',
132
239
  src: 'networkLogic',
@@ -144,6 +251,16 @@ export const editorMachine = setup({
144
251
  'offline': {actions: emit({type: 'offline'})},
145
252
  'loading': {actions: emit({type: 'loading'})},
146
253
  'done loading': {actions: emit({type: 'done loading'})},
254
+ 'update schema': {actions: 'assign schema'},
255
+ 'key down': {
256
+ actions: ['handle behavior event'],
257
+ },
258
+ 'insert text': {
259
+ actions: ['apply:insert text'],
260
+ },
261
+ 'insert text block': {
262
+ actions: ['apply:insert text block'],
263
+ },
147
264
  },
148
265
  initial: 'pristine',
149
266
  states: {
@@ -24,7 +24,6 @@ const debug = debugWithName('hook:useSyncValue')
24
24
  */
25
25
  export interface UseSyncValueProps {
26
26
  editorActor: EditorActor
27
- keyGenerator: () => string
28
27
  portableTextEditor: PortableTextEditor
29
28
  readOnly: boolean
30
29
  }
@@ -52,7 +51,7 @@ export function useSyncValue(
52
51
  value: PortableTextBlock[] | undefined,
53
52
  userCallbackFn?: () => void,
54
53
  ) => void {
55
- const {editorActor, portableTextEditor, readOnly, keyGenerator} = props
54
+ const {editorActor, portableTextEditor, readOnly} = props
56
55
  const {schemaTypes} = portableTextEditor
57
56
  const previousValue = useRef<PortableTextBlock[] | undefined>()
58
57
  const slateEditor = useSlate()
@@ -162,7 +161,7 @@ export function useSyncValue(
162
161
  const validation = validateValue(
163
162
  validationValue,
164
163
  schemaTypes,
165
- keyGenerator,
164
+ editorActor.getSnapshot().context.keyGenerator,
166
165
  )
167
166
  // Resolve validations that can be resolved automatically, without involving the user (but only if the value was changed)
168
167
  if (
@@ -222,7 +221,7 @@ export function useSyncValue(
222
221
  const validation = validateValue(
223
222
  validationValue,
224
223
  schemaTypes,
225
- keyGenerator,
224
+ editorActor.getSnapshot().context.keyGenerator,
226
225
  )
227
226
  if (debug.enabled)
228
227
  debug(
@@ -288,7 +287,6 @@ export function useSyncValue(
288
287
  return updateFunction
289
288
  }, [
290
289
  editorActor,
291
- keyGenerator,
292
290
  portableTextEditor,
293
291
  readOnly,
294
292
  schemaTypes,
@@ -0,0 +1,6 @@
1
+ import {randomKey} from '@sanity/util/content'
2
+
3
+ /**
4
+ * @public
5
+ */
6
+ export const defaultKeyGenerator = (): string => randomKey(12)
@@ -38,14 +38,15 @@ import {
38
38
  KEY_TO_VALUE_ELEMENT,
39
39
  SLATE_TO_PORTABLE_TEXT_RANGE,
40
40
  } from '../../utils/weakMaps'
41
+ import type {EditorActor} from '../editor-machine'
41
42
  import type {PortableTextEditor} from '../PortableTextEditor'
42
43
 
43
44
  const debug = debugWithName('API:editable')
44
45
 
45
46
  export function createWithEditableAPI(
47
+ editorActor: EditorActor,
46
48
  portableTextEditor: PortableTextEditor,
47
49
  types: PortableTextMemberSchemaTypes,
48
- keyGenerator: () => string,
49
50
  ) {
50
51
  return function withEditableAPI(
51
52
  editor: PortableTextSlateEditor,
@@ -151,11 +152,11 @@ export function createWithEditableAPI(
151
152
  const block = toSlateValue(
152
153
  [
153
154
  {
154
- _key: keyGenerator(),
155
+ _key: editorActor.getSnapshot().context.keyGenerator(),
155
156
  _type: types.block.name,
156
157
  children: [
157
158
  {
158
- _key: keyGenerator(),
159
+ _key: editorActor.getSnapshot().context.keyGenerator(),
159
160
  _type: type.name,
160
161
  ...(value ? value : {}),
161
162
  },
@@ -199,7 +200,7 @@ export function createWithEditableAPI(
199
200
  const block = toSlateValue(
200
201
  [
201
202
  {
202
- _key: keyGenerator(),
203
+ _key: editorActor.getSnapshot().context.keyGenerator(),
203
204
  _type: type.name,
204
205
  ...(value ? value : {}),
205
206
  },
@@ -469,7 +470,9 @@ export function createWithEditableAPI(
469
470
  continue
470
471
  }
471
472
 
472
- const annotationKey = keyGenerator()
473
+ const annotationKey = editorActor
474
+ .getSnapshot()
475
+ .context.keyGenerator()
473
476
  const markDefs = block.markDefs ?? []
474
477
  const existingMarkDef = markDefs.find(
475
478
  (markDef) =>
@@ -96,6 +96,7 @@ export function createWithHotkeys(
96
96
  at: nextPath,
97
97
  },
98
98
  )
99
+ Transforms.select(editor, {path: [...nextPath, 0], offset: 0})
99
100
  editor.onChange()
100
101
  return
101
102
  }
@@ -185,38 +186,6 @@ export function createWithHotkeys(
185
186
  return
186
187
  }
187
188
  }
188
- // Block object enter key
189
- if (focusBlock && Editor.isVoid(editor, focusBlock)) {
190
- Editor.insertNode(editor, editor.pteCreateTextBlock({decorators: []}))
191
- event.preventDefault()
192
- editor.onChange()
193
- return
194
- }
195
- // Default enter key behavior
196
- event.preventDefault()
197
- editor.insertBreak()
198
- editor.onChange()
199
- }
200
-
201
- // Soft line breaks
202
- if (isShiftEnter) {
203
- event.preventDefault()
204
- editor.insertText('\n')
205
- return
206
- }
207
-
208
- // Undo/redo
209
- if (isHotkey('mod+z', event.nativeEvent)) {
210
- event.preventDefault()
211
- editor.undo()
212
- return
213
- }
214
- if (
215
- isHotkey('mod+y', event.nativeEvent) ||
216
- isHotkey('mod+shift+z', event.nativeEvent)
217
- ) {
218
- event.preventDefault()
219
- editor.redo()
220
189
  }
221
190
  }
222
191
  return editor
@@ -5,10 +5,11 @@ import type {
5
5
  PortableTextSlateEditor,
6
6
  } from '../../types/editor'
7
7
  import type {SlateTextBlock, VoidElement} from '../../types/slate'
8
+ import type {EditorActor} from '../editor-machine'
8
9
 
9
10
  export function createWithInsertBreak(
11
+ editorActor: EditorActor,
10
12
  types: PortableTextMemberSchemaTypes,
11
- keyGenerator: () => string,
12
13
  ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
13
14
  return function withInsertBreak(
14
15
  editor: PortableTextSlateEditor,
@@ -175,7 +176,10 @@ export function createWithInsertBreak(
175
176
  ) {
176
177
  // This annotation is both present in the previous block
177
178
  // and this block, so let's assign a new key to it
178
- newMarkDefKeys.set(mark, keyGenerator())
179
+ newMarkDefKeys.set(
180
+ mark,
181
+ editorActor.getSnapshot().context.keyGenerator(),
182
+ )
179
183
  }
180
184
  }
181
185
 
@@ -25,7 +25,6 @@ const debug = debugWithName('plugin:withInsertData')
25
25
  export function createWithInsertData(
26
26
  editorActor: EditorActor,
27
27
  schemaTypes: PortableTextMemberSchemaTypes,
28
- keyGenerator: () => string,
29
28
  ) {
30
29
  return function withInsertData(
31
30
  editor: PortableTextSlateEditor,
@@ -149,12 +148,16 @@ export function createWithInsertData(
149
148
  const slateValue = _regenerateKeys(
150
149
  editor,
151
150
  toSlateValue(parsed, {schemaTypes}),
152
- keyGenerator,
151
+ editorActor.getSnapshot().context.keyGenerator,
153
152
  spanTypeName,
154
153
  schemaTypes,
155
154
  )
156
155
  // Validate the result
157
- const validation = validateValue(parsed, schemaTypes, keyGenerator)
156
+ const validation = validateValue(
157
+ parsed,
158
+ schemaTypes,
159
+ editorActor.getSnapshot().context.keyGenerator,
160
+ )
158
161
  // Bail out if it's not valid
159
162
  if (!validation.valid && !validation.resolution?.autoResolve) {
160
163
  const errorDescription = `${validation.resolution?.description}`
@@ -224,7 +227,7 @@ export function createWithInsertData(
224
227
  const validation = validateValue(
225
228
  portableText,
226
229
  schemaTypes,
227
- keyGenerator,
230
+ editorActor.getSnapshot().context.keyGenerator,
228
231
  )
229
232
 
230
233
  // Bail out if it's not valid
@@ -14,7 +14,6 @@ import type {EditorActor} from '../editor-machine'
14
14
  export function createWithObjectKeys(
15
15
  editorActor: EditorActor,
16
16
  schemaTypes: PortableTextMemberSchemaTypes,
17
- keyGenerator: () => string,
18
17
  ) {
19
18
  return function withKeys(
20
19
  editor: PortableTextSlateEditor,
@@ -48,7 +47,7 @@ export function createWithObjectKeys(
48
47
  ...operation,
49
48
  properties: {
50
49
  ...operation.properties,
51
- _key: keyGenerator(),
50
+ _key: editorActor.getSnapshot().context.keyGenerator(),
52
51
  },
53
52
  })
54
53
 
@@ -61,7 +60,7 @@ export function createWithObjectKeys(
61
60
  ...operation,
62
61
  node: {
63
62
  ...operation.node,
64
- _key: keyGenerator(),
63
+ _key: editorActor.getSnapshot().context.keyGenerator(),
65
64
  },
66
65
  })
67
66
 
@@ -78,7 +77,11 @@ export function createWithObjectKeys(
78
77
  // Set key on block itself
79
78
  if (!node._key) {
80
79
  editorActor.send({type: 'normalizing'})
81
- Transforms.setNodes(editor, {_key: keyGenerator()}, {at: path})
80
+ Transforms.setNodes(
81
+ editor,
82
+ {_key: editorActor.getSnapshot().context.keyGenerator()},
83
+ {at: path},
84
+ )
82
85
  editorActor.send({type: 'done normalizing'})
83
86
  return
84
87
  }
@@ -86,7 +89,11 @@ export function createWithObjectKeys(
86
89
  for (const [child, childPath] of Node.children(editor, path)) {
87
90
  if (!child._key) {
88
91
  editorActor.send({type: 'normalizing'})
89
- Transforms.setNodes(editor, {_key: keyGenerator()}, {at: childPath})
92
+ Transforms.setNodes(
93
+ editor,
94
+ {_key: editorActor.getSnapshot().context.keyGenerator()},
95
+ {at: childPath},
96
+ )
90
97
  editorActor.send({type: 'done normalizing'})
91
98
  return
92
99
  }
@@ -81,7 +81,6 @@ export interface PatchFunctions {
81
81
 
82
82
  interface Options {
83
83
  editorActor: EditorActor
84
- keyGenerator: () => string
85
84
  patches$?: PatchObservable
86
85
  patchFunctions: PatchFunctions
87
86
  readOnly: boolean