@portabletext/editor 1.1.9 → 1.1.11

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.
@@ -1,6 +1,7 @@
1
1
  import type {Patch} from '@portabletext/patches'
2
2
  import type {PortableTextBlock} from '@sanity/types'
3
3
  import type {FocusEvent} from 'react'
4
+ import {Editor} from 'slate'
4
5
  import {
5
6
  assertEvent,
6
7
  assign,
@@ -14,14 +15,19 @@ import type {
14
15
  EditorSelection,
15
16
  InvalidValueResolution,
16
17
  PortableTextMemberSchemaTypes,
18
+ PortableTextSlateEditor,
17
19
  } from '../types/editor'
18
20
  import {toPortableTextRange} from '../utils/ranges'
19
21
  import {fromSlateValue} from '../utils/values'
20
22
  import {KEY_TO_VALUE_ELEMENT} from '../utils/weakMaps'
21
- import {behaviorActionImplementations} from './behavior/behavior.actions'
23
+ import {
24
+ behaviorActionImplementations,
25
+ performDefaultAction,
26
+ } from './behavior/behavior.actions'
22
27
  import type {
23
28
  Behavior,
24
29
  BehaviorAction,
30
+ BehaviorActionIntend,
25
31
  BehaviorContext,
26
32
  BehaviorEvent,
27
33
  } from './behavior/behavior.types'
@@ -65,8 +71,16 @@ export type MutationEvent = {
65
71
  type EditorEvent =
66
72
  | {type: 'normalizing'}
67
73
  | {type: 'done normalizing'}
68
- | BehaviorEvent
69
- | BehaviorAction
74
+ | {
75
+ type: 'behavior event'
76
+ behaviorEvent: BehaviorEvent
77
+ editor: PortableTextSlateEditor
78
+ }
79
+ | {
80
+ type: 'behavior action intends'
81
+ editor: PortableTextSlateEditor
82
+ actionIntends: Array<BehaviorActionIntend>
83
+ }
70
84
  | {
71
85
  type: 'update schema'
72
86
  schema: PortableTextMemberSchemaTypes
@@ -153,13 +167,19 @@ export const editorMachine = setup({
153
167
  pendingEvents: [],
154
168
  }),
155
169
  'handle behavior event': enqueueActions(({context, event, enqueue}) => {
156
- assertEvent(event, ['key down', 'before insert text'])
170
+ assertEvent(event, ['behavior event'])
171
+
172
+ const defaultAction = {
173
+ ...event.behaviorEvent,
174
+ editor: event.editor,
175
+ } satisfies BehaviorAction
157
176
 
158
177
  const eventBehaviors = context.behaviors.filter(
159
- (behavior) => behavior.on === event.type,
178
+ (behavior) => behavior.on === event.behaviorEvent.type,
160
179
  )
161
180
 
162
181
  if (eventBehaviors.length === 0) {
182
+ performDefaultAction({context, action: defaultAction})
163
183
  return
164
184
  }
165
185
 
@@ -178,6 +198,7 @@ export const editorMachine = setup({
178
198
  console.warn(
179
199
  `Unable to handle event ${event.type} due to missing selection`,
180
200
  )
201
+ performDefaultAction({context, action: defaultAction})
181
202
  return
182
203
  }
183
204
 
@@ -187,32 +208,42 @@ export const editorMachine = setup({
187
208
  selection,
188
209
  } satisfies BehaviorContext
189
210
 
211
+ let behaviorOverwritten = false
212
+
190
213
  for (const eventBehavior of eventBehaviors) {
191
214
  const shouldRun =
192
215
  eventBehavior.guard?.({
193
216
  context: behaviorContext,
194
- event,
217
+ event: event.behaviorEvent,
195
218
  }) ?? true
196
219
 
197
220
  if (!shouldRun) {
198
221
  continue
199
222
  }
200
223
 
201
- const actions = eventBehavior.actions.map((action) =>
202
- action({context: behaviorContext, event}, shouldRun),
224
+ const actionIntendSets = eventBehavior.actions.map((actionSet) =>
225
+ actionSet(
226
+ {context: behaviorContext, event: event.behaviorEvent},
227
+ shouldRun,
228
+ ),
203
229
  )
204
230
 
205
- for (const action of actions) {
206
- if (typeof action !== 'object') {
207
- continue
208
- }
231
+ for (const actionIntends of actionIntendSets) {
232
+ behaviorOverwritten =
233
+ actionIntends.length > 0 &&
234
+ actionIntends.some((actionIntend) => actionIntend.type !== 'effect')
209
235
 
210
236
  enqueue.raise({
211
- ...action,
237
+ type: 'behavior action intends',
212
238
  editor: event.editor,
239
+ actionIntends,
213
240
  })
214
241
  }
215
242
  }
243
+
244
+ if (!behaviorOverwritten) {
245
+ performDefaultAction({context, action: defaultAction})
246
+ }
216
247
  }),
217
248
  },
218
249
  actors: {
@@ -244,23 +275,86 @@ export const editorMachine = setup({
244
275
  'loading': {actions: emit({type: 'loading'})},
245
276
  'done loading': {actions: emit({type: 'done loading'})},
246
277
  'update schema': {actions: 'assign schema'},
247
- 'key down': {
248
- actions: ['handle behavior event'],
249
- },
250
- 'before insert text': {
251
- actions: ['handle behavior event'],
252
- },
253
- 'apply block style': {
254
- actions: [behaviorActionImplementations['apply block style']],
255
- },
256
- 'delete text': {
257
- actions: [behaviorActionImplementations['delete text']],
258
- },
259
- 'insert text': {
260
- actions: [behaviorActionImplementations['insert text']],
261
- },
262
- 'insert text block': {
263
- actions: [behaviorActionImplementations['insert text block']],
278
+ 'behavior event': {actions: 'handle behavior event'},
279
+ 'behavior action intends': {
280
+ actions: [
281
+ ({context, event}) => {
282
+ Editor.withoutNormalizing(event.editor, () => {
283
+ for (const actionIntend of event.actionIntends) {
284
+ const action = {
285
+ ...actionIntend,
286
+ editor: event.editor,
287
+ }
288
+
289
+ switch (action.type) {
290
+ case 'delete backward': {
291
+ behaviorActionImplementations['delete backward']({
292
+ context,
293
+ action,
294
+ })
295
+ break
296
+ }
297
+ case 'delete text': {
298
+ behaviorActionImplementations['delete text']({
299
+ context,
300
+ action,
301
+ })
302
+ break
303
+ }
304
+ case 'insert break': {
305
+ behaviorActionImplementations['insert break']({
306
+ context,
307
+ action,
308
+ })
309
+ break
310
+ }
311
+ case 'insert soft break': {
312
+ behaviorActionImplementations['insert soft break']({
313
+ context,
314
+ action,
315
+ })
316
+ break
317
+ }
318
+ case 'insert text': {
319
+ behaviorActionImplementations['insert text']({
320
+ context,
321
+ action,
322
+ })
323
+ break
324
+ }
325
+ case 'insert text block': {
326
+ behaviorActionImplementations['insert text block']({
327
+ context,
328
+ action,
329
+ })
330
+ break
331
+ }
332
+ case 'set block': {
333
+ behaviorActionImplementations['set block']({
334
+ context,
335
+ action,
336
+ })
337
+ break
338
+ }
339
+ case 'unset block': {
340
+ behaviorActionImplementations['unset block']({
341
+ context,
342
+ action,
343
+ })
344
+ break
345
+ }
346
+ default: {
347
+ behaviorActionImplementations.effect({
348
+ context,
349
+ action,
350
+ })
351
+ }
352
+ }
353
+ }
354
+ })
355
+ event.editor.onChange()
356
+ },
357
+ ],
264
358
  },
265
359
  },
266
360
  initial: 'pristine',
@@ -0,0 +1,55 @@
1
+ import type {Editor} from 'slate'
2
+ import type {EditorActor} from '../editor-machine'
3
+
4
+ export function createWithEventListeners(editorActor: EditorActor) {
5
+ return function withEventListeners(editor: Editor) {
6
+ editor.deleteBackward = (unit) => {
7
+ editorActor.send({
8
+ type: 'behavior event',
9
+ behaviorEvent: {
10
+ type: 'delete backward',
11
+ unit,
12
+ },
13
+ editor,
14
+ })
15
+ return
16
+ }
17
+
18
+ editor.insertBreak = () => {
19
+ editorActor.send({
20
+ type: 'behavior event',
21
+ behaviorEvent: {
22
+ type: 'insert break',
23
+ },
24
+ editor,
25
+ })
26
+ return
27
+ }
28
+
29
+ editor.insertSoftBreak = () => {
30
+ editorActor.send({
31
+ type: 'behavior event',
32
+ behaviorEvent: {
33
+ type: 'insert soft break',
34
+ },
35
+ editor,
36
+ })
37
+ return
38
+ }
39
+
40
+ editor.insertText = (text, options) => {
41
+ editorActor.send({
42
+ type: 'behavior event',
43
+ behaviorEvent: {
44
+ type: 'insert text',
45
+ text,
46
+ options,
47
+ },
48
+ editor,
49
+ })
50
+ return
51
+ }
52
+
53
+ return editor
54
+ }
55
+ }
@@ -75,13 +75,19 @@ export function createWithUtils({
75
75
  }
76
76
  }
77
77
 
78
- editor.pteCreateTextBlock = (options: {decorators: Array<string>}) => {
78
+ editor.pteCreateTextBlock = (options: {
79
+ decorators: Array<string>
80
+ listItem?: string
81
+ level?: number
82
+ }) => {
79
83
  const block = toSlateValue(
80
84
  [
81
85
  {
82
86
  _type: schemaTypes.block.name,
83
87
  _key: editorActor.getSnapshot().context.keyGenerator(),
84
88
  style: schemaTypes.styles[0].value || 'normal',
89
+ ...(options.listItem ? {listItem: options.listItem} : {}),
90
+ ...(options.level ? {level: options.level} : {}),
85
91
  markDefs: [],
86
92
  children: [
87
93
  {
@@ -3,8 +3,8 @@ import type {BaseOperation, Editor, Node, NodeEntry} from 'slate'
3
3
  import type {PortableTextSlateEditor} from '../../types/editor'
4
4
  import type {createEditorOptions} from '../../types/options'
5
5
  import {createOperationToPatches} from '../../utils/operationToPatches'
6
+ import {createWithEventListeners} from './create-with-event-listeners'
6
7
  import {createWithEditableAPI} from './createWithEditableAPI'
7
- import {createWithInsertBreak} from './createWithInsertBreak'
8
8
  import {createWithMaxBlocks} from './createWithMaxBlocks'
9
9
  import {createWithObjectKeys} from './createWithObjectKeys'
10
10
  import {createWithPatches} from './createWithPatches'
@@ -98,8 +98,6 @@ export const withPlugins = <T extends Editor>(
98
98
 
99
99
  const withPlaceholderBlock = createWithPlaceholderBlock()
100
100
 
101
- const withInsertBreak = createWithInsertBreak(editorActor, schemaTypes)
102
-
103
101
  const withUtils = createWithUtils({
104
102
  editorActor,
105
103
  schemaTypes,
@@ -109,6 +107,7 @@ export const withPlugins = <T extends Editor>(
109
107
  editorActor,
110
108
  schemaTypes,
111
109
  )
110
+ const withEventListeners = createWithEventListeners(editorActor)
112
111
 
113
112
  e.destroy = () => {
114
113
  const originalFunctions = originalFnMap.get(e)
@@ -129,9 +128,7 @@ export const withPlugins = <T extends Editor>(
129
128
  withUtils(
130
129
  withPlaceholderBlock(
131
130
  withPortableTextLists(
132
- withPortableTextSelections(
133
- withEditableAPI(withInsertBreak(e)),
134
- ),
131
+ withPortableTextSelections(withEditableAPI(e)),
135
132
  ),
136
133
  ),
137
134
  ),
@@ -145,18 +142,18 @@ export const withPlugins = <T extends Editor>(
145
142
 
146
143
  // Ordering is important here, selection dealing last, data manipulation in the middle and core model stuff first.
147
144
  return {
148
- editor: withSchemaTypes(
149
- withObjectKeys(
150
- withPortableTextMarkModel(
151
- withPortableTextBlockStyle(
152
- withPortableTextLists(
153
- withPlaceholderBlock(
154
- withUtils(
155
- withMaxBlocks(
156
- withUndoRedo(
157
- withPatches(
158
- withPortableTextSelections(
159
- withEditableAPI(withInsertBreak(e)),
145
+ editor: withEventListeners(
146
+ withSchemaTypes(
147
+ withObjectKeys(
148
+ withPortableTextMarkModel(
149
+ withPortableTextBlockStyle(
150
+ withPortableTextLists(
151
+ withPlaceholderBlock(
152
+ withUtils(
153
+ withMaxBlocks(
154
+ withUndoRedo(
155
+ withPatches(
156
+ withPortableTextSelections(withEditableAPI(e)),
160
157
  ),
161
158
  ),
162
159
  ),
package/src/index.ts CHANGED
@@ -5,8 +5,8 @@ export type {
5
5
  BehaviorContext,
6
6
  BehaviorEvent,
7
7
  BehaviorGuard,
8
- RaiseBehaviorActionIntend,
9
8
  PickFromUnion,
9
+ BehaviorActionIntendSet,
10
10
  } from './editor/behavior/behavior.types'
11
11
  export {PortableTextEditable} from './editor/Editable'
12
12
  export type {PortableTextEditableProps} from './editor/Editable'
@@ -222,7 +222,11 @@ export interface PortableTextSlateEditor extends ReactEditor {
222
222
  /**
223
223
  * Helper function that creates a text block
224
224
  */
225
- pteCreateTextBlock: (options: {decorators: Array<string>}) => Descendant
225
+ pteCreateTextBlock: (options: {
226
+ decorators: Array<string>
227
+ listItem?: string
228
+ level?: number
229
+ }) => Descendant
226
230
 
227
231
  /**
228
232
  * Undo
@@ -1,220 +0,0 @@
1
- import {isEqual} from 'lodash'
2
- import {Editor, Node, Path, Range, Transforms} from 'slate'
3
- import type {
4
- PortableTextMemberSchemaTypes,
5
- PortableTextSlateEditor,
6
- } from '../../types/editor'
7
- import type {SlateTextBlock, VoidElement} from '../../types/slate'
8
- import type {EditorActor} from '../editor-machine'
9
-
10
- export function createWithInsertBreak(
11
- editorActor: EditorActor,
12
- types: PortableTextMemberSchemaTypes,
13
- ): (editor: PortableTextSlateEditor) => PortableTextSlateEditor {
14
- return function withInsertBreak(
15
- editor: PortableTextSlateEditor,
16
- ): PortableTextSlateEditor {
17
- const {insertBreak} = editor
18
-
19
- editor.insertBreak = () => {
20
- if (!editor.selection) {
21
- insertBreak()
22
- return
23
- }
24
-
25
- const [focusSpan] = Array.from(
26
- Editor.nodes(editor, {
27
- mode: 'lowest',
28
- at: editor.selection.focus,
29
- match: (n) => editor.isTextSpan(n),
30
- voids: false,
31
- }),
32
- )[0] ?? [undefined]
33
- const focusDecorators =
34
- focusSpan.marks?.filter((mark) =>
35
- types.decorators.some((decorator) => decorator.value === mark),
36
- ) ?? []
37
- const focusAnnotations =
38
- focusSpan.marks?.filter(
39
- (mark) =>
40
- !types.decorators.some((decorator) => decorator.value === mark),
41
- ) ?? []
42
-
43
- const focusBlockPath = editor.selection.focus.path.slice(0, 1)
44
- const focusBlock = Node.descendant(editor, focusBlockPath) as
45
- | SlateTextBlock
46
- | VoidElement
47
-
48
- if (editor.isTextBlock(focusBlock)) {
49
- const [start, end] = Range.edges(editor.selection)
50
- const atTheStartOfBlock = isEqual(end, {
51
- path: [...focusBlockPath, 0],
52
- offset: 0,
53
- })
54
-
55
- if (atTheStartOfBlock && Range.isCollapsed(editor.selection)) {
56
- Editor.insertNode(
57
- editor,
58
- editor.pteCreateTextBlock({
59
- decorators: focusAnnotations.length === 0 ? focusDecorators : [],
60
- }),
61
- )
62
-
63
- const [nextBlockPath] = Path.next(focusBlockPath)
64
-
65
- Transforms.select(editor, {
66
- anchor: {path: [nextBlockPath, 0], offset: 0},
67
- focus: {path: [nextBlockPath, 0], offset: 0},
68
- })
69
-
70
- return
71
- }
72
-
73
- const lastFocusBlockChild =
74
- focusBlock.children[focusBlock.children.length - 1]
75
- const atTheEndOfBlock = isEqual(start, {
76
- path: [...focusBlockPath, focusBlock.children.length - 1],
77
- offset: editor.isTextSpan(lastFocusBlockChild)
78
- ? lastFocusBlockChild.text.length
79
- : 0,
80
- })
81
-
82
- if (atTheEndOfBlock && Range.isCollapsed(editor.selection)) {
83
- Editor.insertNode(
84
- editor,
85
- editor.pteCreateTextBlock({
86
- decorators: [],
87
- }),
88
- )
89
-
90
- const [nextBlockPath] = Path.next(focusBlockPath)
91
-
92
- Transforms.setSelection(editor, {
93
- anchor: {path: [nextBlockPath, 0], offset: 0},
94
- focus: {path: [nextBlockPath, 0], offset: 0},
95
- })
96
-
97
- return
98
- }
99
-
100
- const isInTheMiddleOfNode = !atTheStartOfBlock && !atTheEndOfBlock
101
-
102
- if (isInTheMiddleOfNode) {
103
- Editor.withoutNormalizing(editor, () => {
104
- if (!editor.selection) {
105
- return
106
- }
107
-
108
- Transforms.splitNodes(editor, {
109
- at: editor.selection,
110
- })
111
-
112
- const [nextNode, nextNodePath] = Editor.node(
113
- editor,
114
- Path.next(focusBlockPath),
115
- {depth: 1},
116
- )
117
-
118
- Transforms.setSelection(editor, {
119
- anchor: {path: [...nextNodePath, 0], offset: 0},
120
- focus: {path: [...nextNodePath, 0], offset: 0},
121
- })
122
-
123
- /**
124
- * Assign new keys to markDefs that are now split across two blocks
125
- */
126
- if (
127
- editor.isTextBlock(nextNode) &&
128
- nextNode.markDefs &&
129
- nextNode.markDefs.length > 0
130
- ) {
131
- const newMarkDefKeys = new Map<string, string>()
132
-
133
- const prevNodeSpans = Array.from(
134
- Node.children(editor, focusBlockPath),
135
- )
136
- .map((entry) => entry[0])
137
- .filter((node) => editor.isTextSpan(node))
138
- const children = Node.children(editor, nextNodePath)
139
-
140
- for (const [child, childPath] of children) {
141
- if (!editor.isTextSpan(child)) {
142
- continue
143
- }
144
-
145
- const marks = child.marks ?? []
146
-
147
- // Go through the marks of the span and figure out if any of
148
- // them refer to annotations that are also present in the
149
- // previous block
150
- for (const mark of marks) {
151
- if (
152
- types.decorators.some(
153
- (decorator) => decorator.value === mark,
154
- )
155
- ) {
156
- continue
157
- }
158
-
159
- if (
160
- prevNodeSpans.some((prevNodeSpan) =>
161
- prevNodeSpan.marks?.includes(mark),
162
- ) &&
163
- !newMarkDefKeys.has(mark)
164
- ) {
165
- // This annotation is both present in the previous block
166
- // and this block, so let's assign a new key to it
167
- newMarkDefKeys.set(
168
- mark,
169
- editorActor.getSnapshot().context.keyGenerator(),
170
- )
171
- }
172
- }
173
-
174
- const newMarks = marks.map(
175
- (mark) => newMarkDefKeys.get(mark) ?? mark,
176
- )
177
-
178
- // No need to update the marks if they are the same
179
- if (!isEqual(marks, newMarks)) {
180
- Transforms.setNodes(
181
- editor,
182
- {marks: newMarks},
183
- {
184
- at: childPath,
185
- },
186
- )
187
- }
188
- }
189
-
190
- // Time to update all the markDefs that need a new key because
191
- // they've been split across blocks
192
- const newMarkDefs = nextNode.markDefs.map((markDef) => ({
193
- ...markDef,
194
- _key: newMarkDefKeys.get(markDef._key) ?? markDef._key,
195
- }))
196
-
197
- // No need to update the markDefs if they are the same
198
- if (!isEqual(nextNode.markDefs, newMarkDefs)) {
199
- Transforms.setNodes(
200
- editor,
201
- {markDefs: newMarkDefs},
202
- {
203
- at: nextNodePath,
204
- match: (node) => editor.isTextBlock(node),
205
- },
206
- )
207
- }
208
- }
209
- })
210
- editor.onChange()
211
- return
212
- }
213
- }
214
-
215
- insertBreak()
216
- }
217
-
218
- return editor
219
- }
220
- }