@portabletext/editor 1.1.2 → 1.1.4

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -61,7 +61,6 @@
61
61
  "@sanity/diff-match-patch": "^3.1.1",
62
62
  "@sanity/pkg-utils": "^6.11.2",
63
63
  "@sanity/schema": "^3.55.0",
64
- "@sanity/test": "0.0.1-alpha.1",
65
64
  "@sanity/types": "^3.55.0",
66
65
  "@sanity/ui": "^2.8.9",
67
66
  "@sanity/util": "^3.55.0",
@@ -36,7 +36,6 @@ import {
36
36
  type RenderLeafProps,
37
37
  } from 'slate-react'
38
38
  import type {
39
- EditorChange,
40
39
  EditorSelection,
41
40
  OnCopyFn,
42
41
  OnPasteFn,
@@ -603,7 +602,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
603
602
  // Set the correct range
604
603
  domSelection.addRange(newDOMRange)
605
604
  }
606
- } catch (error) {
605
+ } catch {
607
606
  debug(`Could not resolve selection, selecting top document`)
608
607
  // Deselect the editor
609
608
  Transforms.deselect(slateEditor)
@@ -656,7 +655,7 @@ export const PortableTextEditable = forwardRef(function PortableTextEditable(
656
655
  return noop
657
656
  }
658
657
  // Translate PortableTextEditor prop fn to Slate plugin fn
659
- return (editor: ReactEditor, domRange: Range) => {
658
+ return (_editor: ReactEditor, domRange: Range) => {
660
659
  scrollSelectionIntoView(portableTextEditor, domRange)
661
660
  }
662
661
  }, [portableTextEditor, scrollSelectionIntoView])
@@ -11,14 +11,13 @@ import type {
11
11
  } from '@sanity/types'
12
12
  import {Component, type MutableRefObject, type PropsWithChildren} from 'react'
13
13
  import {Subject} from 'rxjs'
14
- import {createActor, type Subscription} from 'xstate'
14
+ import {createActor} from 'xstate'
15
15
  import type {
16
16
  EditableAPI,
17
17
  EditableAPIDeleteOptions,
18
18
  EditorChange,
19
19
  EditorChanges,
20
20
  EditorSelection,
21
- MutationChange,
22
21
  PatchObservable,
23
22
  PortableTextMemberSchemaTypes,
24
23
  } from '../types/editor'
@@ -315,7 +314,7 @@ export class PortableTextEditor extends Component<PortableTextEditorProps> {
315
314
  ) => {
316
315
  return editor.editable?.isVoid(element)
317
316
  }
318
- static isObjectPath = (editor: PortableTextEditor, path: Path): boolean => {
317
+ static isObjectPath = (_editor: PortableTextEditor, path: Path): boolean => {
319
318
  if (!path || !Array.isArray(path)) return false
320
319
  const isChildObjectEditPath = path.length > 3 && path[1] === 'children'
321
320
  const isBlockObjectEditPath = path.length > 1 && path[1] !== 'children'
@@ -5,7 +5,7 @@ import {
5
5
  useEffect,
6
6
  useState,
7
7
  } from 'react'
8
- import type {EditorChanges, EditorSelection} from '../../types/editor'
8
+ import type {EditorSelection} from '../../types/editor'
9
9
  import {debugWithName} from '../../utils/debug'
10
10
  import type {EditorActor} from '../editor-machine'
11
11
 
@@ -271,18 +271,16 @@ export function createWithEditableAPI(
271
271
  hasBlockStyle: (style: string): boolean => {
272
272
  try {
273
273
  return editor.pteHasBlockStyle(style)
274
- } catch (err) {
274
+ } catch {
275
275
  // This is fine.
276
- // debug(err)
277
276
  return false
278
277
  }
279
278
  },
280
279
  hasListStyle: (listStyle: string): boolean => {
281
280
  try {
282
281
  return editor.pteHasListStyle(listStyle)
283
- } catch (err) {
282
+ } catch {
284
283
  // This is fine.
285
- // debug(err)
286
284
  return false
287
285
  }
288
286
  },
@@ -341,7 +339,7 @@ export function createWithEditableAPI(
341
339
  }) || [],
342
340
  )[0] || [undefined]
343
341
  node = ReactEditor.toDOMNode(editor, item)
344
- } catch (err) {
342
+ } catch {
345
343
  // Nothing
346
344
  }
347
345
  return node
@@ -376,7 +374,7 @@ export function createWithEditableAPI(
376
374
  }
377
375
  }
378
376
  return activeAnnotations
379
- } catch (err) {
377
+ } catch {
380
378
  return []
381
379
  }
382
380
  },
@@ -427,7 +425,7 @@ export function createWithEditableAPI(
427
425
 
428
426
  return spanMarkDefs?.includes(annotationType)
429
427
  })
430
- } catch (err) {
428
+ } catch {
431
429
  return false
432
430
  }
433
431
  },
@@ -21,6 +21,24 @@ export function createWithInsertBreak(
21
21
  return
22
22
  }
23
23
 
24
+ const [focusSpan] = Array.from(
25
+ Editor.nodes(editor, {
26
+ mode: 'lowest',
27
+ at: editor.selection.focus,
28
+ match: (n) => editor.isTextSpan(n),
29
+ voids: false,
30
+ }),
31
+ )[0] ?? [undefined]
32
+ const focusDecorators =
33
+ focusSpan.marks?.filter((mark) =>
34
+ types.decorators.some((decorator) => decorator.value === mark),
35
+ ) ?? []
36
+ const focusAnnotations =
37
+ focusSpan.marks?.filter(
38
+ (mark) =>
39
+ !types.decorators.some((decorator) => decorator.value === mark),
40
+ ) ?? []
41
+
24
42
  const focusBlockPath = editor.selection.focus.path.slice(0, 1)
25
43
  const focusBlock = Node.descendant(editor, focusBlockPath) as
26
44
  | SlateTextBlock
@@ -34,15 +52,11 @@ export function createWithInsertBreak(
34
52
  })
35
53
 
36
54
  if (isEndAtStartOfBlock && Range.isCollapsed(editor.selection)) {
37
- const focusDecorators = editor.isTextSpan(focusBlock.children[0])
38
- ? (focusBlock.children[0].marks ?? []).filter((mark) =>
39
- types.decorators.some((decorator) => decorator.value === mark),
40
- )
41
- : []
42
-
43
55
  Editor.insertNode(
44
56
  editor,
45
- editor.pteCreateTextBlock({decorators: focusDecorators}),
57
+ editor.pteCreateTextBlock({
58
+ decorators: focusAnnotations.length === 0 ? focusDecorators : [],
59
+ }),
46
60
  )
47
61
 
48
62
  const [nextBlockPath] = Path.next(focusBlockPath)
@@ -64,6 +78,36 @@ export function createWithInsertBreak(
64
78
  ? lastFocusBlockChild.text.length
65
79
  : 0,
66
80
  })
81
+
82
+ if (
83
+ isStartAtEndOfBlock &&
84
+ Range.isCollapsed(editor.selection) &&
85
+ focusDecorators.length > 0 &&
86
+ focusAnnotations.length > 0
87
+ ) {
88
+ Editor.withoutNormalizing(editor, () => {
89
+ if (!editor.selection) {
90
+ return
91
+ }
92
+
93
+ Editor.insertNode(
94
+ editor,
95
+ editor.pteCreateTextBlock({
96
+ decorators: [],
97
+ }),
98
+ )
99
+
100
+ const [nextBlockPath] = Path.next(focusBlockPath)
101
+
102
+ Transforms.setSelection(editor, {
103
+ anchor: {path: [nextBlockPath, 0], offset: 0},
104
+ focus: {path: [nextBlockPath, 0], offset: 0},
105
+ })
106
+ })
107
+ editor.onChange()
108
+ return
109
+ }
110
+
67
111
  const isInTheMiddleOfNode = !isEndAtStartOfBlock && !isStartAtEndOfBlock
68
112
 
69
113
  if (isInTheMiddleOfNode) {
@@ -4,7 +4,6 @@ import {isEqual, uniq} from 'lodash'
4
4
  import {Editor, Range, Transforms, type Descendant, type Node} from 'slate'
5
5
  import {ReactEditor} from 'slate-react'
6
6
  import type {
7
- EditorChanges,
8
7
  PortableTextMemberSchemaTypes,
9
8
  PortableTextSlateEditor,
10
9
  } from '../../types/editor'
@@ -5,18 +5,9 @@
5
5
  */
6
6
 
7
7
  import {isPortableTextBlock, isPortableTextSpan} from '@portabletext/toolkit'
8
- import type {PortableTextObject} from '@sanity/types'
8
+ import type {PortableTextObject, PortableTextSpan} from '@sanity/types'
9
9
  import {isEqual, uniq} from 'lodash'
10
- import {
11
- Editor,
12
- Element,
13
- Node,
14
- Path,
15
- Range,
16
- Text,
17
- Transforms,
18
- type Descendant,
19
- } from 'slate'
10
+ import {Editor, Element, Node, Path, Range, Text, Transforms} from 'slate'
20
11
  import type {
21
12
  PortableTextMemberSchemaTypes,
22
13
  PortableTextSlateEditor,
@@ -290,44 +281,125 @@ export function createWithPortableTextMarkModel(
290
281
  return
291
282
  }
292
283
 
293
- // Special hook before inserting text at the end of an annotation.
294
284
  if (op.type === 'insert_text') {
295
285
  const {selection} = editor
296
- if (
297
- selection &&
298
- Range.isCollapsed(selection) &&
299
- Editor.marks(editor)?.marks?.some(
300
- (mark) => !decorators.includes(mark),
286
+ const collapsedSelection = selection
287
+ ? Range.isCollapsed(selection)
288
+ : false
289
+
290
+ if (selection && collapsedSelection) {
291
+ const [_block, blockPath] = Editor.node(editor, selection, {
292
+ depth: 1,
293
+ })
294
+
295
+ const [span, spanPath] =
296
+ Array.from(
297
+ Editor.nodes(editor, {
298
+ mode: 'lowest',
299
+ at: selection.focus,
300
+ match: (n) => editor.isTextSpan(n),
301
+ voids: false,
302
+ }),
303
+ )[0] ?? ([undefined, undefined] as const)
304
+
305
+ const marks = span.marks ?? []
306
+ const marksWithoutAnnotations = marks.filter((mark) =>
307
+ decorators.includes(mark),
301
308
  )
302
- ) {
303
- const [node] = Array.from(
304
- Editor.nodes(editor, {
305
- mode: 'lowest',
306
- at: selection.focus,
307
- match: (n) =>
308
- (n as unknown as Descendant)._type === types.span.name,
309
- voids: false,
310
- }),
311
- )[0] || [undefined]
312
- if (
313
- Text.isText(node) &&
314
- node.text.length === selection.focus.offset &&
315
- Array.isArray(node.marks) &&
316
- node.marks.length > 0
317
- ) {
318
- const marksWithoutAnnotationMarks: string[] = (
319
- {
320
- ...(Editor.marks(editor) || {}),
321
- }.marks || []
322
- ).filter((mark) => decorators.includes(mark))
323
- Transforms.insertNodes(editor, {
324
- _type: 'span',
325
- _key: keyGenerator(),
326
- text: op.text,
327
- marks: marksWithoutAnnotationMarks,
328
- })
329
- debug('Inserting text at end of annotation')
330
- return
309
+ const spanHasAnnotations =
310
+ marks.length > marksWithoutAnnotations.length
311
+
312
+ const spanIsEmpty = span.text.length === 0
313
+
314
+ const atTheBeginningOfSpan = selection.anchor.offset === 0
315
+ const atTheEndOfSpan = selection.anchor.offset === span.text.length
316
+
317
+ let previousSpan: PortableTextSpan | undefined
318
+ let nextSpan: PortableTextSpan | undefined
319
+
320
+ for (const [child, childPath] of Node.children(editor, blockPath, {
321
+ reverse: true,
322
+ })) {
323
+ if (!editor.isTextSpan(child)) {
324
+ continue
325
+ }
326
+
327
+ if (Path.isBefore(childPath, spanPath)) {
328
+ previousSpan = child
329
+ break
330
+ }
331
+ }
332
+
333
+ for (const [child, childPath] of Node.children(editor, blockPath)) {
334
+ if (!editor.isTextSpan(child)) {
335
+ continue
336
+ }
337
+
338
+ if (Path.isAfter(childPath, spanPath)) {
339
+ nextSpan = child
340
+ break
341
+ }
342
+ }
343
+
344
+ const previousSpanHasSameAnnotation = previousSpan
345
+ ? previousSpan.marks?.some(
346
+ (mark) => !decorators.includes(mark) && marks.includes(mark),
347
+ )
348
+ : false
349
+ const previousSpanHasSameMarks = previousSpan
350
+ ? previousSpan.marks?.every((mark) => marks.includes(mark))
351
+ : false
352
+ const nextSpanHasSameAnnotation = nextSpan
353
+ ? nextSpan.marks?.some(
354
+ (mark) => !decorators.includes(mark) && marks.includes(mark),
355
+ )
356
+ : false
357
+ const nextSpanHasSameMarks = nextSpan
358
+ ? nextSpan.marks?.every((mark) => marks.includes(mark))
359
+ : false
360
+
361
+ if (spanHasAnnotations && !spanIsEmpty) {
362
+ if (atTheBeginningOfSpan) {
363
+ if (previousSpanHasSameMarks) {
364
+ Transforms.insertNodes(editor, {
365
+ _type: 'span',
366
+ _key: keyGenerator(),
367
+ text: op.text,
368
+ marks: previousSpan?.marks ?? [],
369
+ })
370
+ } else if (previousSpanHasSameAnnotation) {
371
+ apply(op)
372
+ } else {
373
+ Transforms.insertNodes(editor, {
374
+ _type: 'span',
375
+ _key: keyGenerator(),
376
+ text: op.text,
377
+ marks: [],
378
+ })
379
+ }
380
+ return
381
+ }
382
+
383
+ if (atTheEndOfSpan) {
384
+ if (nextSpanHasSameMarks) {
385
+ Transforms.insertNodes(editor, {
386
+ _type: 'span',
387
+ _key: keyGenerator(),
388
+ text: op.text,
389
+ marks: nextSpan?.marks ?? [],
390
+ })
391
+ } else if (nextSpanHasSameAnnotation) {
392
+ apply(op)
393
+ } else {
394
+ Transforms.insertNodes(editor, {
395
+ _type: 'span',
396
+ _key: keyGenerator(),
397
+ text: op.text,
398
+ marks: [],
399
+ })
400
+ }
401
+ return
402
+ }
331
403
  }
332
404
  }
333
405
  }
@@ -1,6 +1,5 @@
1
1
  import {render, waitFor} from '@testing-library/react'
2
2
  import {createRef, type RefObject} from 'react'
3
- import * as React from 'react'
4
3
  import {describe, expect, it, vi} from 'vitest'
5
4
  import {
6
5
  PortableTextEditorTester,
@@ -47,8 +47,6 @@ const debugVerbose = debug.enabled && true
47
47
  export function createApplyPatch(
48
48
  schemaTypes: PortableTextMemberSchemaTypes,
49
49
  ): (editor: PortableTextSlateEditor, patch: Patch) => boolean {
50
- let previousPatch: Patch | undefined
51
-
52
50
  return (editor: PortableTextSlateEditor, patch: Patch): boolean => {
53
51
  let changed = false
54
52
 
@@ -66,7 +64,7 @@ export function createApplyPatch(
66
64
  changed = insertPatch(editor, patch, schemaTypes)
67
65
  break
68
66
  case 'unset':
69
- changed = unsetPatch(editor, patch, previousPatch)
67
+ changed = unsetPatch(editor, patch)
70
68
  break
71
69
  case 'set':
72
70
  changed = setPatch(editor, patch)
@@ -80,7 +78,7 @@ export function createApplyPatch(
80
78
  } catch (err) {
81
79
  console.error(err)
82
80
  }
83
- previousPatch = patch
81
+
84
82
  return changed
85
83
  }
86
84
  }
@@ -308,18 +306,14 @@ function setPatch(editor: PortableTextSlateEditor, patch: SetPatch) {
308
306
  return true
309
307
  }
310
308
 
311
- function unsetPatch(
312
- editor: PortableTextSlateEditor,
313
- patch: UnsetPatch,
314
- previousPatch?: Patch,
315
- ) {
309
+ function unsetPatch(editor: PortableTextSlateEditor, patch: UnsetPatch) {
316
310
  // Value
317
311
  if (patch.path.length === 0) {
318
312
  debug('Removing everything')
319
313
  debugState(editor, 'before')
320
314
  const previousSelection = editor.selection
321
315
  Transforms.deselect(editor)
322
- editor.children.forEach((c, i) => {
316
+ editor.children.forEach((_child, i) => {
323
317
  Transforms.removeNodes(editor, {at: [i]})
324
318
  })
325
319
  Transforms.insertNodes(editor, editor.pteCreateTextBlock({decorators: []}))