@portabletext/editor 1.18.6 → 1.19.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/lib/_chunks-cjs/behavior.core.cjs +52 -35
  2. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  3. package/lib/_chunks-es/behavior.core.js +52 -35
  4. package/lib/_chunks-es/behavior.core.js.map +1 -1
  5. package/lib/behaviors/index.cjs +1 -0
  6. package/lib/behaviors/index.cjs.map +1 -1
  7. package/lib/behaviors/index.d.cts +76 -84
  8. package/lib/behaviors/index.d.ts +76 -84
  9. package/lib/behaviors/index.js +3 -2
  10. package/lib/index.cjs +234 -251
  11. package/lib/index.cjs.map +1 -1
  12. package/lib/index.d.cts +257 -1106
  13. package/lib/index.d.ts +257 -1106
  14. package/lib/index.js +235 -252
  15. package/lib/index.js.map +1 -1
  16. package/lib/selectors/index.cjs +12 -9
  17. package/lib/selectors/index.cjs.map +1 -1
  18. package/lib/selectors/index.js +12 -9
  19. package/lib/selectors/index.js.map +1 -1
  20. package/package.json +7 -7
  21. package/src/behavior-actions/behavior.actions.ts +28 -36
  22. package/src/behaviors/behavior.core.decorators.ts +36 -42
  23. package/src/behaviors/behavior.core.ts +4 -3
  24. package/src/behaviors/behavior.types.ts +38 -24
  25. package/src/behaviors/index.ts +1 -0
  26. package/src/editor/PortableTextEditor.tsx +14 -16
  27. package/src/editor/__tests__/self-solving.test.tsx +4 -11
  28. package/src/editor/create-editor.ts +1 -3
  29. package/src/editor/editor-machine.ts +37 -41
  30. package/src/editor/plugins/create-with-event-listeners.ts +44 -57
  31. package/src/editor/plugins/createWithHotKeys.ts +1 -11
  32. package/src/editor/plugins/createWithPortableTextMarkModel.ts +12 -1
  33. package/src/editor/plugins/createWithPortableTextSelections.ts +1 -5
  34. package/src/editor/with-applying-behavior-actions.ts +15 -0
  35. package/src/selectors/selector.get-selected-spans.test.ts +122 -0
  36. package/src/selectors/selector.get-selected-spans.ts +3 -1
  37. package/src/selectors/selector.is-active-decorator.test.ts +65 -0
@@ -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'
@@ -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
 
@@ -152,6 +153,7 @@ export type InternalEditorEmittedEvent =
152
153
  description: string
153
154
  data: unknown
154
155
  }
156
+ | {type: 'select'; selection: EditorSelection}
155
157
  | {type: 'selection'; selection: EditorSelection}
156
158
  | {type: 'blurred'; event: FocusEvent<HTMLDivElement, Element>}
157
159
  | {type: 'focused'; event: FocusEvent<HTMLDivElement, Element>}
@@ -164,10 +166,7 @@ export type InternalEditorEmittedEvent =
164
166
  'type',
165
167
  | 'annotation.add'
166
168
  | 'annotation.remove'
167
- | 'annotation.toggle'
168
169
  | 'blur'
169
- | 'decorator.add'
170
- | 'decorator.remove'
171
170
  | 'decorator.toggle'
172
171
  | 'insert.block object'
173
172
  | 'insert.inline object'
@@ -263,10 +262,12 @@ export const editorMachine = setup({
263
262
  return
264
263
  }
265
264
 
266
- Editor.withoutNormalizing(event.editor, () => {
267
- performAction({
268
- context,
269
- action: defaultAction,
265
+ withApplyingBehaviorActions(event.editor, () => {
266
+ Editor.withoutNormalizing(event.editor, () => {
267
+ performAction({
268
+ context,
269
+ action: defaultAction,
270
+ })
270
271
  })
271
272
  })
272
273
  event.editor.onChange()
@@ -324,32 +325,28 @@ export const editorMachine = setup({
324
325
  (actionIntend) => actionIntend.type !== 'effect',
325
326
  ))
326
327
 
327
- Editor.withoutNormalizing(event.editor, () => {
328
- for (const actionIntend of actionIntends) {
329
- const action = {
330
- ...actionIntend,
331
- editor: event.editor,
332
- }
328
+ withApplyingBehaviorActions(event.editor, () => {
329
+ Editor.withoutNormalizing(event.editor, () => {
330
+ for (const actionIntend of actionIntends) {
331
+ if (actionIntend.type === 'raise') {
332
+ enqueue.raise({
333
+ type: 'behavior event',
334
+ behaviorEvent: actionIntend.event,
335
+ editor: event.editor,
336
+ })
337
+ continue
338
+ }
333
339
 
334
- performAction({context, action})
335
- }
336
- })
337
- event.editor.onChange()
340
+ const action = {
341
+ ...actionIntend,
342
+ editor: event.editor,
343
+ }
338
344
 
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
- ),
345
+ performAction({context, action})
346
+ }
351
347
  })
352
- }
348
+ })
349
+ event.editor.onChange()
353
350
  }
354
351
 
355
352
  if (behaviorOverwritten) {
@@ -363,10 +360,12 @@ export const editorMachine = setup({
363
360
  return
364
361
  }
365
362
 
366
- Editor.withoutNormalizing(event.editor, () => {
367
- performAction({
368
- context,
369
- action: defaultAction,
363
+ withApplyingBehaviorActions(event.editor, () => {
364
+ Editor.withoutNormalizing(event.editor, () => {
365
+ performAction({
366
+ context,
367
+ action: defaultAction,
368
+ })
370
369
  })
371
370
  })
372
371
  event.editor.onChange()
@@ -450,13 +449,7 @@ export const editorMachine = setup({
450
449
  'behavior event': {
451
450
  actions: 'handle behavior event',
452
451
  },
453
- 'annotation.add': {
454
- actions: emit(({event}) => event),
455
- },
456
- 'annotation.remove': {
457
- actions: emit(({event}) => event),
458
- },
459
- 'annotation.toggle': {
452
+ 'annotation.*': {
460
453
  actions: emit(({event}) => event),
461
454
  },
462
455
  'blur': {
@@ -474,6 +467,9 @@ export const editorMachine = setup({
474
467
  'list item.*': {
475
468
  actions: emit(({event}) => event),
476
469
  },
470
+ 'select': {
471
+ actions: emit(({event}) => event),
472
+ },
477
473
  'style.*': {
478
474
  actions: emit(({event}) => event),
479
475
  },
@@ -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
+ }
@@ -0,0 +1,122 @@
1
+ import {expect, test} from 'vitest'
2
+ import {getSelectedSpans, type EditorSchema, type EditorSelection} from '.'
3
+ import type {EditorSnapshot} from '../editor/editor-snapshot'
4
+
5
+ test(getSelectedSpans.name, () => {
6
+ function snapshot(selection: EditorSelection): EditorSnapshot {
7
+ return {
8
+ context: {
9
+ schema: {} as EditorSchema,
10
+ keyGenerator: () => '',
11
+ activeDecorators: [],
12
+ value: [
13
+ {
14
+ _type: 'block',
15
+ _key: 'b1',
16
+ children: [
17
+ {
18
+ _type: 'span',
19
+ _key: 's1',
20
+ text: 'foo',
21
+ marks: ['strong'],
22
+ },
23
+ {
24
+ _type: 'span',
25
+ _key: 's2',
26
+ text: 'bar',
27
+ },
28
+ ],
29
+ },
30
+ {
31
+ _type: 'image',
32
+ _key: 'b2',
33
+ },
34
+ {
35
+ _type: 'block',
36
+ _key: 'b3',
37
+ children: [
38
+ {
39
+ _type: 'span',
40
+ _key: 's3',
41
+ text: 'baz',
42
+ },
43
+ ],
44
+ },
45
+ ],
46
+ selection,
47
+ },
48
+ }
49
+ }
50
+
51
+ expect(
52
+ getSelectedSpans(
53
+ snapshot({
54
+ anchor: {
55
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
56
+ offset: 0,
57
+ },
58
+ focus: {
59
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
60
+ offset: 3,
61
+ },
62
+ }),
63
+ ),
64
+ ).toEqual([
65
+ {
66
+ node: {_type: 'span', _key: 's1', text: 'foo', marks: ['strong']},
67
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
68
+ },
69
+ ])
70
+
71
+ expect(
72
+ getSelectedSpans(
73
+ snapshot({
74
+ anchor: {
75
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
76
+ offset: 2,
77
+ },
78
+ focus: {
79
+ path: [{_key: 'b1'}, 'children', {_key: 's2'}],
80
+ offset: 3,
81
+ },
82
+ }),
83
+ ),
84
+ ).toEqual([
85
+ {
86
+ node: {_type: 'span', _key: 's1', text: 'foo', marks: ['strong']},
87
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
88
+ },
89
+ {
90
+ node: {_type: 'span', _key: 's2', text: 'bar'},
91
+ path: [{_key: 'b1'}, 'children', {_key: 's2'}],
92
+ },
93
+ ])
94
+
95
+ expect(
96
+ getSelectedSpans(
97
+ snapshot({
98
+ anchor: {
99
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
100
+ offset: 2,
101
+ },
102
+ focus: {
103
+ path: [{_key: 'b3'}, 'children', {_key: 's3'}],
104
+ offset: 2,
105
+ },
106
+ }),
107
+ ),
108
+ ).toEqual([
109
+ {
110
+ node: {_type: 'span', _key: 's1', text: 'foo', marks: ['strong']},
111
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
112
+ },
113
+ {
114
+ node: {_type: 'span', _key: 's2', text: 'bar'},
115
+ path: [{_key: 'b1'}, 'children', {_key: 's2'}],
116
+ },
117
+ {
118
+ node: {_type: 'span', _key: 's3', text: 'baz'},
119
+ path: [{_key: 'b3'}, 'children', {_key: 's3'}],
120
+ },
121
+ ])
122
+ })
@@ -67,9 +67,11 @@ export const getSelectedSpans: EditorSelector<
67
67
  path: [{_key: block._key}, 'children', {_key: child._key}],
68
68
  })
69
69
 
70
- if (startBlockKey === endBlockKey) {
70
+ if (startSpanKey === endSpanKey) {
71
71
  break
72
72
  }
73
+
74
+ continue
73
75
  }
74
76
 
75
77
  if (endSpanKey && child._key === endSpanKey) {
@@ -0,0 +1,65 @@
1
+ import {expect, test} from 'vitest'
2
+ import type {EditorSchema, EditorSelection} from '.'
3
+ import type {EditorSnapshot} from '../editor/editor-snapshot'
4
+ import {isActiveDecorator} from './selector.is-active-decorator'
5
+
6
+ test(isActiveDecorator.name, () => {
7
+ function snapshot(selection: EditorSelection): EditorSnapshot {
8
+ return {
9
+ context: {
10
+ schema: {} as EditorSchema,
11
+ keyGenerator: () => '',
12
+ activeDecorators: [],
13
+ value: [
14
+ {
15
+ _type: '_block',
16
+ _key: 'b1',
17
+ children: [
18
+ {
19
+ _type: 'span',
20
+ _key: 's1',
21
+ text: 'foo',
22
+ marks: ['strong'],
23
+ },
24
+ {
25
+ _type: 'span',
26
+ _key: 's2',
27
+ text: 'bar',
28
+ },
29
+ ],
30
+ },
31
+ ],
32
+ selection,
33
+ },
34
+ }
35
+ }
36
+
37
+ expect(
38
+ isActiveDecorator('strong')(
39
+ snapshot({
40
+ anchor: {
41
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
42
+ offset: 0,
43
+ },
44
+ focus: {
45
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
46
+ offset: 3,
47
+ },
48
+ }),
49
+ ),
50
+ ).toBe(true)
51
+ expect(
52
+ isActiveDecorator('strong')(
53
+ snapshot({
54
+ anchor: {
55
+ path: [{_key: 'b1'}, 'children', {_key: 's1'}],
56
+ offset: 2,
57
+ },
58
+ focus: {
59
+ path: [{_key: 'b1'}, 'children', {_key: 's2'}],
60
+ offset: 3,
61
+ },
62
+ }),
63
+ ),
64
+ ).toBe(false)
65
+ })