@portabletext/editor 1.18.7 → 1.20.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 (43) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs +52 -35
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-cjs/selector.is-selection-collapsed.cjs.map +1 -1
  4. package/lib/_chunks-es/behavior.core.js +52 -35
  5. package/lib/_chunks-es/behavior.core.js.map +1 -1
  6. package/lib/_chunks-es/selector.is-selection-collapsed.js.map +1 -1
  7. package/lib/behaviors/index.cjs +1 -0
  8. package/lib/behaviors/index.cjs.map +1 -1
  9. package/lib/behaviors/index.d.cts +78 -86
  10. package/lib/behaviors/index.d.ts +78 -86
  11. package/lib/behaviors/index.js +3 -2
  12. package/lib/behaviors/index.js.map +1 -1
  13. package/lib/index.cjs +277 -422
  14. package/lib/index.cjs.map +1 -1
  15. package/lib/index.d.cts +488 -1127
  16. package/lib/index.d.ts +488 -1127
  17. package/lib/index.js +279 -425
  18. package/lib/index.js.map +1 -1
  19. package/lib/selectors/index.cjs +12 -9
  20. package/lib/selectors/index.cjs.map +1 -1
  21. package/lib/selectors/index.js +12 -9
  22. package/lib/selectors/index.js.map +1 -1
  23. package/package.json +5 -7
  24. package/src/behavior-actions/behavior.actions.ts +28 -36
  25. package/src/behaviors/behavior.core.decorators.ts +36 -42
  26. package/src/behaviors/behavior.core.ts +4 -3
  27. package/src/behaviors/behavior.types.ts +40 -26
  28. package/src/behaviors/index.ts +1 -0
  29. package/src/editor/PortableTextEditor.tsx +14 -16
  30. package/src/editor/__tests__/self-solving.test.tsx +4 -11
  31. package/src/editor/components/Element.tsx +17 -23
  32. package/src/editor/create-editor.ts +18 -3
  33. package/src/editor/editor-machine.ts +67 -45
  34. package/src/editor/nodes/DefaultObject.tsx +2 -2
  35. package/src/editor/plugins/create-with-event-listeners.ts +44 -57
  36. package/src/editor/plugins/createWithHotKeys.ts +1 -11
  37. package/src/editor/plugins/createWithPortableTextMarkModel.ts +12 -1
  38. package/src/editor/plugins/createWithPortableTextSelections.ts +1 -5
  39. package/src/editor/with-applying-behavior-actions.ts +15 -0
  40. package/src/selectors/selector.get-selected-spans.test.ts +122 -0
  41. package/src/selectors/selector.get-selected-spans.ts +3 -1
  42. package/src/selectors/selector.is-active-decorator.test.ts +65 -0
  43. package/src/editor/nodes/index.ts +0 -189
@@ -30,11 +30,6 @@ import {debugWithName} from '../../utils/debug'
30
30
  import {fromSlateValue} from '../../utils/values'
31
31
  import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
32
32
  import ObjectNode from '../nodes/DefaultObject'
33
- import {
34
- DefaultBlockObject,
35
- DefaultListItem,
36
- DefaultListItemInner,
37
- } from '../nodes/index'
38
33
  import {DraggableBlock} from './DraggableBlock'
39
34
 
40
35
  const debug = debugWithName('components:Element')
@@ -185,12 +180,14 @@ export const Element: FunctionComponent<ElementProps> = ({
185
180
  })
186
181
  }
187
182
  let level: number | undefined
183
+
188
184
  if (isListItem) {
189
185
  if (typeof element.level === 'number') {
190
186
  level = element.level
191
187
  }
192
188
  className += ` pt-list-item pt-list-item-${element.listItem} pt-list-item-level-${level || 1}`
193
189
  }
190
+
194
191
  if (editor.isListBlock(value) && isListItem && element.listItem) {
195
192
  const listType = schemaTypes.lists.find(
196
193
  (item) => item.value === element.listItem,
@@ -207,17 +204,9 @@ export const Element: FunctionComponent<ElementProps> = ({
207
204
  level: value.level || 1,
208
205
  editorElementRef: blockRef,
209
206
  })
210
- } else {
211
- renderedBlock = (
212
- <DefaultListItem
213
- listStyle={value.listItem || schemaTypes.lists[0].value}
214
- listLevel={value.level || 1}
215
- >
216
- <DefaultListItemInner>{renderedBlock}</DefaultListItemInner>
217
- </DefaultListItem>
218
- )
219
207
  }
220
208
  }
209
+
221
210
  const renderProps: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
222
211
  {
223
212
  children: renderedBlock,
@@ -263,24 +252,31 @@ export const Element: FunctionComponent<ElementProps> = ({
263
252
  </div>
264
253
  )
265
254
  }
255
+
266
256
  const schemaType = schemaTypes.blockObjects.find(
267
257
  (_type) => _type.name === element._type,
268
258
  )
259
+
269
260
  if (!schemaType) {
270
261
  throw new Error(
271
262
  `Could not find schema type for block element of _type ${element._type}`,
272
263
  )
273
264
  }
265
+
274
266
  if (debugRenders) {
275
267
  debug(`Render ${element._key} (object block)`)
276
268
  }
269
+
277
270
  className = 'pt-block pt-object-block'
271
+
278
272
  const block = fromSlateValue(
279
273
  [element],
280
274
  schemaTypes.block.name,
281
275
  KEY_TO_VALUE_ELEMENT.get(editor),
282
276
  )[0]
277
+
283
278
  let renderedBlockFromProps: JSX.Element | undefined
279
+
284
280
  if (renderBlock) {
285
281
  const _props: Omit<BlockRenderProps, 'type'> = Object.defineProperty(
286
282
  {
@@ -305,20 +301,18 @@ export const Element: FunctionComponent<ElementProps> = ({
305
301
  )
306
302
  renderedBlockFromProps = renderBlock(_props as BlockRenderProps)
307
303
  }
304
+
308
305
  return (
309
306
  <div key={element._key} {...attributes} className={className}>
310
307
  {children}
311
308
  <DraggableBlock element={element} readOnly={readOnly} blockRef={blockRef}>
312
- {renderedBlockFromProps && (
313
- <div ref={blockRef} contentEditable={false}>
314
- {renderedBlockFromProps}
315
- </div>
316
- )}
317
- {!renderedBlockFromProps && (
318
- <DefaultBlockObject selected={selected}>
309
+ <div ref={blockRef} contentEditable={false}>
310
+ {renderedBlockFromProps ? (
311
+ renderedBlockFromProps
312
+ ) : (
319
313
  <ObjectNode value={value} />
320
- </DefaultBlockObject>
321
- )}
314
+ )}
315
+ </div>
322
316
  </DraggableBlock>
323
317
  </div>
324
318
  )
@@ -63,15 +63,13 @@ export type EditorEvent = PickFromUnion<
63
63
  'type',
64
64
  | 'annotation.add'
65
65
  | 'annotation.remove'
66
- | 'annotation.toggle'
67
66
  | 'blur'
68
- | 'decorator.add'
69
- | 'decorator.remove'
70
67
  | 'decorator.toggle'
71
68
  | 'focus'
72
69
  | 'insert.block object'
73
70
  | 'insert.inline object'
74
71
  | 'list item.toggle'
72
+ | 'select'
75
73
  | 'style.toggle'
76
74
  | 'patches'
77
75
  | 'update behaviors'
@@ -84,6 +82,10 @@ export type EditorEvent = PickFromUnion<
84
82
  */
85
83
  export type Editor = {
86
84
  getSnapshot: () => EditorSnapshot
85
+ /**
86
+ * @beta
87
+ */
88
+ registerBehavior: (config: {behavior: Behavior}) => () => void
87
89
  send: (event: EditorEvent) => void
88
90
  on: ActorRef<Snapshot<unknown>, EventObject, EditorEmittedEvent>['on']
89
91
  _internal: {
@@ -137,6 +139,19 @@ function createEditorFromActor(editorActor: EditorActor): Editor {
137
139
  editorActorSnapshot: editorActor.getSnapshot(),
138
140
  slateEditorInstance: slateEditor.instance,
139
141
  }),
142
+ registerBehavior: (config) => {
143
+ editorActor.send({
144
+ type: 'add behavior',
145
+ behavior: config.behavior,
146
+ })
147
+
148
+ return () => {
149
+ editorActor.send({
150
+ type: 'remove behavior',
151
+ behavior: config.behavior,
152
+ })
153
+ }
154
+ },
140
155
  send: (event) => {
141
156
  editorActor.send(event)
142
157
  },
@@ -30,6 +30,7 @@ import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
30
30
  import type {EditorSchema} from './define-schema'
31
31
  import type {EditorContext} from './editor-snapshot'
32
32
  import {getActiveDecorators} from './get-active-decorators'
33
+ import {withApplyingBehaviorActions} from './with-applying-behavior-actions'
33
34
 
34
35
  export * from 'xstate/guards'
35
36
 
@@ -78,6 +79,14 @@ export type InternalEditorEvent =
78
79
  editor: PortableTextSlateEditor
79
80
  nativeEvent?: {preventDefault: () => void}
80
81
  }
82
+ | {
83
+ type: 'add behavior'
84
+ behavior: Behavior
85
+ }
86
+ | {
87
+ type: 'remove behavior'
88
+ behavior: Behavior
89
+ }
81
90
  | {
82
91
  type: 'update readOnly'
83
92
  readOnly: boolean
@@ -152,6 +161,7 @@ export type InternalEditorEmittedEvent =
152
161
  description: string
153
162
  data: unknown
154
163
  }
164
+ | {type: 'select'; selection: EditorSelection}
155
165
  | {type: 'selection'; selection: EditorSelection}
156
166
  | {type: 'blurred'; event: FocusEvent<HTMLDivElement, Element>}
157
167
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
@@ -164,10 +174,7 @@ export type InternalEditorEmittedEvent =
164
174
  'type',
165
175
  | 'annotation.add'
166
176
  | 'annotation.remove'
167
- | 'annotation.toggle'
168
177
  | 'blur'
169
- | 'decorator.add'
170
- | 'decorator.remove'
171
178
  | 'decorator.toggle'
172
179
  | 'insert.block object'
173
180
  | 'insert.inline object'
@@ -182,7 +189,7 @@ export type InternalEditorEmittedEvent =
182
189
  export const editorMachine = setup({
183
190
  types: {
184
191
  context: {} as {
185
- behaviors: Array<Behavior>
192
+ behaviors: Set<Behavior>
186
193
  keyGenerator: () => string
187
194
  pendingEvents: Array<PatchEvent | MutationEvent>
188
195
  schema: EditorSchema
@@ -203,10 +210,26 @@ export const editorMachine = setup({
203
210
  },
204
211
  },
205
212
  actions: {
213
+ 'add behavior to context': assign({
214
+ behaviors: ({context, event}) => {
215
+ assertEvent(event, 'add behavior')
216
+
217
+ return new Set([...context.behaviors, event.behavior])
218
+ },
219
+ }),
220
+ 'remove behavior from context': assign({
221
+ behaviors: ({context, event}) => {
222
+ assertEvent(event, 'remove behavior')
223
+
224
+ context.behaviors.delete(event.behavior)
225
+
226
+ return new Set([...context.behaviors])
227
+ },
228
+ }),
206
229
  'assign behaviors': assign({
207
230
  behaviors: ({event}) => {
208
231
  assertEvent(event, 'update behaviors')
209
- return event.behaviors
232
+ return new Set(event.behaviors)
210
233
  },
211
234
  }),
212
235
  'assign schema': assign({
@@ -254,7 +277,7 @@ export const editorMachine = setup({
254
277
  editor: event.editor,
255
278
  } satisfies BehaviorAction)
256
279
 
257
- const eventBehaviors = context.behaviors.filter(
280
+ const eventBehaviors = [...context.behaviors.values()].filter(
258
281
  (behavior) => behavior.on === event.behaviorEvent.type,
259
282
  )
260
283
 
@@ -263,10 +286,12 @@ export const editorMachine = setup({
263
286
  return
264
287
  }
265
288
 
266
- Editor.withoutNormalizing(event.editor, () => {
267
- performAction({
268
- context,
269
- action: defaultAction,
289
+ withApplyingBehaviorActions(event.editor, () => {
290
+ Editor.withoutNormalizing(event.editor, () => {
291
+ performAction({
292
+ context,
293
+ action: defaultAction,
294
+ })
270
295
  })
271
296
  })
272
297
  event.editor.onChange()
@@ -324,32 +349,28 @@ export const editorMachine = setup({
324
349
  (actionIntend) => actionIntend.type !== 'effect',
325
350
  ))
326
351
 
327
- Editor.withoutNormalizing(event.editor, () => {
328
- for (const actionIntend of actionIntends) {
329
- const action = {
330
- ...actionIntend,
331
- editor: event.editor,
332
- }
352
+ withApplyingBehaviorActions(event.editor, () => {
353
+ Editor.withoutNormalizing(event.editor, () => {
354
+ for (const actionIntend of actionIntends) {
355
+ if (actionIntend.type === 'raise') {
356
+ enqueue.raise({
357
+ type: 'behavior event',
358
+ behaviorEvent: actionIntend.event,
359
+ editor: event.editor,
360
+ })
361
+ continue
362
+ }
333
363
 
334
- performAction({context, action})
335
- }
336
- })
337
- event.editor.onChange()
364
+ const action = {
365
+ ...actionIntend,
366
+ editor: event.editor,
367
+ }
338
368
 
339
- if (
340
- actionIntends.some(
341
- (actionIntend) => actionIntend.type === 'reselect',
342
- )
343
- ) {
344
- enqueue.raise({
345
- type: 'selection',
346
- selection: toPortableTextRange(
347
- event.editor.children,
348
- event.editor.selection,
349
- context.schema,
350
- ),
369
+ performAction({context, action})
370
+ }
351
371
  })
352
- }
372
+ })
373
+ event.editor.onChange()
353
374
  }
354
375
 
355
376
  if (behaviorOverwritten) {
@@ -363,10 +384,12 @@ export const editorMachine = setup({
363
384
  return
364
385
  }
365
386
 
366
- Editor.withoutNormalizing(event.editor, () => {
367
- performAction({
368
- context,
369
- action: defaultAction,
387
+ withApplyingBehaviorActions(event.editor, () => {
388
+ Editor.withoutNormalizing(event.editor, () => {
389
+ performAction({
390
+ context,
391
+ action: defaultAction,
392
+ })
370
393
  })
371
394
  })
372
395
  event.editor.onChange()
@@ -376,7 +399,7 @@ export const editorMachine = setup({
376
399
  }).createMachine({
377
400
  id: 'editor',
378
401
  context: ({input}) => ({
379
- behaviors: input.behaviors ?? coreBehaviors,
402
+ behaviors: new Set(input.behaviors ?? coreBehaviors),
380
403
  keyGenerator: input.keyGenerator,
381
404
  pendingEvents: [],
382
405
  schema: input.schema,
@@ -386,6 +409,8 @@ export const editorMachine = setup({
386
409
  value: input.value,
387
410
  }),
388
411
  on: {
412
+ 'add behavior': {actions: 'add behavior to context'},
413
+ 'remove behavior': {actions: 'remove behavior from context'},
389
414
  'unset': {actions: emit(({event}) => event)},
390
415
  'value changed': {actions: emit(({event}) => event)},
391
416
  'invalid value': {actions: emit(({event}) => event)},
@@ -450,13 +475,7 @@ export const editorMachine = setup({
450
475
  'behavior event': {
451
476
  actions: 'handle behavior event',
452
477
  },
453
- 'annotation.add': {
454
- actions: emit(({event}) => event),
455
- },
456
- 'annotation.remove': {
457
- actions: emit(({event}) => event),
458
- },
459
- 'annotation.toggle': {
478
+ 'annotation.*': {
460
479
  actions: emit(({event}) => event),
461
480
  },
462
481
  'blur': {
@@ -474,6 +493,9 @@ export const editorMachine = setup({
474
493
  'list item.*': {
475
494
  actions: emit(({event}) => event),
476
495
  },
496
+ 'select': {
497
+ actions: emit(({event}) => event),
498
+ },
477
499
  'style.*': {
478
500
  actions: emit(({event}) => event),
479
501
  },
@@ -7,8 +7,8 @@ type Props = {
7
7
 
8
8
  const DefaultObject = (props: Props): JSX.Element => {
9
9
  return (
10
- <div>
11
- <pre>{JSON.stringify(props.value, null, 2)}</pre>
10
+ <div style={{userSelect: 'none'}}>
11
+ [{props.value._type}: {props.value._key}]
12
12
  </div>
13
13
  )
14
14
  }
@@ -1,5 +1,9 @@
1
- import type {Editor} from 'slate'
1
+ import {Editor} from 'slate'
2
+ import {toPortableTextRange} from '../../utils/ranges'
3
+ import {fromSlateValue} from '../../utils/values'
4
+ import {KEY_TO_VALUE_ELEMENT} from '../../utils/weakMaps'
2
5
  import type {EditorActor} from '../editor-machine'
6
+ import {isApplyingBehaviorActions} from '../with-applying-behavior-actions'
3
7
 
4
8
  export function createWithEventListeners(
5
9
  editorActor: EditorActor,
@@ -35,17 +39,6 @@ export function createWithEventListeners(
35
39
  })
36
40
  break
37
41
  }
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
42
  case 'blur': {
50
43
  editorActor.send({
51
44
  type: 'behavior event',
@@ -56,28 +49,6 @@ export function createWithEventListeners(
56
49
  })
57
50
  break
58
51
  }
59
- case 'decorator.add': {
60
- editorActor.send({
61
- type: 'behavior event',
62
- behaviorEvent: {
63
- type: 'decorator.add',
64
- decorator: event.decorator,
65
- },
66
- editor,
67
- })
68
- break
69
- }
70
- case 'decorator.remove': {
71
- editorActor.send({
72
- type: 'behavior event',
73
- behaviorEvent: {
74
- type: 'decorator.remove',
75
- decorator: event.decorator,
76
- },
77
- editor,
78
- })
79
- break
80
- }
81
52
  case 'decorator.toggle': {
82
53
  editorActor.send({
83
54
  type: 'behavior event',
@@ -133,6 +104,17 @@ export function createWithEventListeners(
133
104
  })
134
105
  break
135
106
  }
107
+ case 'select': {
108
+ editorActor.send({
109
+ type: 'behavior event',
110
+ behaviorEvent: {
111
+ type: 'select',
112
+ selection: event.selection,
113
+ },
114
+ editor,
115
+ })
116
+ break
117
+ }
136
118
  case 'style.toggle': {
137
119
  editorActor.send({
138
120
  type: 'behavior event',
@@ -152,29 +134,7 @@ export function createWithEventListeners(
152
134
  }
153
135
  })
154
136
 
155
- editor.addMark = (mark) => {
156
- editorActor.send({
157
- type: 'behavior event',
158
- behaviorEvent: {
159
- type: 'decorator.add',
160
- decorator: mark,
161
- },
162
- editor,
163
- })
164
- return
165
- }
166
-
167
- editor.removeMark = (mark) => {
168
- editorActor.send({
169
- type: 'behavior event',
170
- behaviorEvent: {
171
- type: 'decorator.remove',
172
- decorator: mark,
173
- },
174
- editor,
175
- })
176
- return
177
- }
137
+ const {select} = editor
178
138
 
179
139
  editor.deleteBackward = (unit) => {
180
140
  editorActor.send({
@@ -235,6 +195,33 @@ export function createWithEventListeners(
235
195
  return
236
196
  }
237
197
 
198
+ editor.select = (location) => {
199
+ if (isApplyingBehaviorActions(editor)) {
200
+ select(location)
201
+ return
202
+ }
203
+
204
+ const range = Editor.range(editor, location)
205
+
206
+ editorActor.send({
207
+ type: 'behavior event',
208
+ behaviorEvent: {
209
+ type: 'select',
210
+ selection: toPortableTextRange(
211
+ fromSlateValue(
212
+ editor.children,
213
+ editorActor.getSnapshot().context.schema.block.name,
214
+ KEY_TO_VALUE_ELEMENT.get(editor),
215
+ ),
216
+ range,
217
+ editorActor.getSnapshot().context.schema,
218
+ ),
219
+ },
220
+ editor,
221
+ })
222
+ return
223
+ }
224
+
238
225
  return editor
239
226
  }
240
227
  }
@@ -9,16 +9,6 @@ import type {PortableTextEditor} from '../PortableTextEditor'
9
9
 
10
10
  const debug = debugWithName('plugin:withHotKeys')
11
11
 
12
- const DEFAULT_HOTKEYS: HotkeyOptions = {
13
- marks: {
14
- 'mod+b': 'strong',
15
- 'mod+i': 'em',
16
- 'mod+u': 'underline',
17
- "mod+'": 'code',
18
- },
19
- custom: {},
20
- }
21
-
22
12
  /**
23
13
  * This plugin takes care of all hotkeys in the editor
24
14
  *
@@ -29,7 +19,7 @@ export function createWithHotkeys(
29
19
  hotkeysFromOptions?: HotkeyOptions,
30
20
  ): (editor: PortableTextSlateEditor & ReactEditor) => any {
31
21
  const reservedHotkeys = ['enter', 'tab', 'shift', 'delete', 'end']
32
- const activeHotkeys = hotkeysFromOptions || DEFAULT_HOTKEYS // TODO: Merge where possible? A union?
22
+ const activeHotkeys = hotkeysFromOptions ?? {}
33
23
  return function withHotKeys(editor: PortableTextSlateEditor & ReactEditor) {
34
24
  editor.pteWithHotKeys = (event: KeyboardEvent<HTMLDivElement>): void => {
35
25
  // Wire up custom marks hotkeys
@@ -741,7 +741,12 @@ export const addDecoratorActionImplementation: BehaviorActionImplementation<
741
741
  editor.marks = marks as Text
742
742
  }
743
743
  }
744
- editor.onChange()
744
+
745
+ if (editor.selection) {
746
+ // Reselect
747
+ const selection = editor.selection
748
+ editor.selection = {...selection}
749
+ }
745
750
  }
746
751
  }
747
752
 
@@ -823,6 +828,12 @@ export const removeDecoratorActionImplementation: BehaviorActionImplementation<
823
828
  editor.marks = {marks: marks.marks, _type: 'span'} as Text
824
829
  }
825
830
  }
831
+
832
+ if (editor.selection) {
833
+ // Reselect
834
+ const selection = editor.selection
835
+ editor.selection = {...selection}
836
+ }
826
837
  }
827
838
  }
828
839
 
@@ -55,12 +55,8 @@ export function createWithPortableTextSelections(
55
55
 
56
56
  const {onChange} = editor
57
57
  editor.onChange = () => {
58
- const hasChanges = editor.operations.length > 0
59
58
  onChange()
60
- if (
61
- hasChanges &&
62
- !editorActor.getSnapshot().matches({setup: 'setting up'})
63
- ) {
59
+ if (!editorActor.getSnapshot().matches({setup: 'setting up'})) {
64
60
  emitPortableTextSelection()
65
61
  }
66
62
  }
@@ -0,0 +1,15 @@
1
+ import type {Editor} from 'slate'
2
+
3
+ const IS_APPLYING_BEHAVIOR_ACTIONS: WeakMap<Editor, boolean | undefined> =
4
+ new WeakMap()
5
+
6
+ export function withApplyingBehaviorActions(editor: Editor, fn: () => void) {
7
+ const prev = isApplyingBehaviorActions(editor)
8
+ IS_APPLYING_BEHAVIOR_ACTIONS.set(editor, true)
9
+ fn()
10
+ IS_APPLYING_BEHAVIOR_ACTIONS.set(editor, prev)
11
+ }
12
+
13
+ export function isApplyingBehaviorActions(editor: Editor) {
14
+ return IS_APPLYING_BEHAVIOR_ACTIONS.get(editor) ?? false
15
+ }