@portabletext/editor 1.55.14 → 1.55.16

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.55.14",
3
+ "version": "1.55.16",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -320,6 +320,10 @@ export const coreDndBehaviors = [
320
320
  originEvent,
321
321
  },
322
322
  ) => [
323
+ raise({
324
+ type: 'select',
325
+ at: dropPosition,
326
+ }),
323
327
  ...(draggingEntireBlocks
324
328
  ? draggedBlocks.map((block) =>
325
329
  raise({
@@ -333,10 +337,6 @@ export const coreDndBehaviors = [
333
337
  at: dragOrigin.selection,
334
338
  }),
335
339
  ]),
336
- raise({
337
- type: 'select',
338
- at: dropPosition,
339
- }),
340
340
  raise({
341
341
  type: 'insert.blocks',
342
342
  blocks: event.data,
@@ -266,7 +266,14 @@ export const PortableTextEditable = forwardRef<
266
266
  debug(
267
267
  `Normalized selection from props ${JSON.stringify(normalizedSelection)}`,
268
268
  )
269
- const slateRange = toSlateRange(normalizedSelection, slateEditor)
269
+ const slateRange = toSlateRange({
270
+ context: {
271
+ schema: editorActor.getSnapshot().context.schema,
272
+ value: slateEditor.value,
273
+ selection: normalizedSelection,
274
+ },
275
+ blockIndexMap: slateEditor.blockIndexMap,
276
+ })
270
277
  if (slateRange) {
271
278
  Transforms.select(slateEditor, slateRange)
272
279
  // Output selection here in those cases where the editor selection was the same, and there are no set_selection operations made.
@@ -45,7 +45,7 @@ function getBlockNodes(
45
45
  return []
46
46
  }
47
47
 
48
- const range = toSlateRange(snapshot.context.selection, slateEditor)
48
+ const range = toSlateRange(snapshot)
49
49
 
50
50
  if (!range) {
51
51
  return []
@@ -76,7 +76,7 @@ function getChildNodes(
76
76
  return []
77
77
  }
78
78
 
79
- const range = toSlateRange(snapshot.context.selection, slateEditor)
79
+ const range = toSlateRange(snapshot)
80
80
 
81
81
  if (!range) {
82
82
  return []
@@ -118,7 +118,7 @@ export function getSelectionDomNodes({
118
118
  }
119
119
  }
120
120
 
121
- const range = toSlateRange(snapshot.context.selection, slateEditor)
121
+ const range = toSlateRange(snapshot)
122
122
 
123
123
  if (!range) {
124
124
  return {
@@ -136,25 +136,22 @@ describe('plugin:withEditableAPI: .delete()', () => {
136
136
  await waitFor(() => {
137
137
  if (editorRef.current) {
138
138
  // New keys here confirms that a placeholder block has been created
139
- expect(PortableTextEditor.getValue(editorRef.current))
140
- .toMatchInlineSnapshot(`
141
- [
142
- {
143
- "_key": "k2",
144
- "_type": "myTestBlockType",
145
- "children": [
146
- {
147
- "_key": "k3",
148
- "_type": "span",
149
- "marks": [],
150
- "text": "",
151
- },
152
- ],
153
- "markDefs": [],
154
- "style": "normal",
155
- },
156
- ]
157
- `)
139
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
140
+ {
141
+ _key: 'k2',
142
+ _type: 'myTestBlockType',
143
+ children: [
144
+ {
145
+ _key: 'k3',
146
+ _type: 'span',
147
+ marks: [],
148
+ text: '',
149
+ },
150
+ ],
151
+ markDefs: [],
152
+ style: 'normal',
153
+ },
154
+ ])
158
155
  }
159
156
  })
160
157
  })
@@ -15,6 +15,8 @@ import {
15
15
  } from 'slate'
16
16
  import type {DOMNode} from 'slate-dom'
17
17
  import {ReactEditor} from 'slate-react'
18
+ import {buildIndexMaps} from '../../internal-utils/build-index-maps'
19
+ import {createPlaceholderBlock} from '../../internal-utils/create-placeholder-block'
18
20
  import {debugWithName} from '../../internal-utils/debug'
19
21
  import {toSlateRange} from '../../internal-utils/ranges'
20
22
  import {
@@ -131,12 +133,21 @@ export function createEditableAPI(
131
133
  })
132
134
  },
133
135
  select: (selection: EditorSelection): void => {
134
- const slateSelection = toSlateRange(selection, editor)
136
+ const slateSelection = toSlateRange({
137
+ context: {
138
+ schema: editorActor.getSnapshot().context.schema,
139
+ value: editor.value,
140
+ selection,
141
+ },
142
+ blockIndexMap: editor.blockIndexMap,
143
+ })
144
+
135
145
  if (slateSelection) {
136
146
  Transforms.select(editor, slateSelection)
137
147
  } else {
138
148
  Transforms.deselect(editor)
139
149
  }
150
+
140
151
  editor.onChange()
141
152
  },
142
153
  focusBlock: (): PortableTextBlock | undefined => {
@@ -312,10 +323,15 @@ export function createEditableAPI(
312
323
  PortableTextBlock | PortableTextChild | undefined,
313
324
  Path | undefined,
314
325
  ] => {
315
- const slatePath = toSlateRange(
316
- {focus: {path, offset: 0}, anchor: {path, offset: 0}},
317
- editor,
318
- )
326
+ const slatePath = toSlateRange({
327
+ context: {
328
+ schema: editorActor.getSnapshot().context.schema,
329
+ value: editor.value,
330
+ selection: {focus: {path, offset: 0}, anchor: {path, offset: 0}},
331
+ },
332
+ blockIndexMap: editor.blockIndexMap,
333
+ })
334
+
319
335
  if (slatePath) {
320
336
  const [block, blockPath] = Editor.node(
321
337
  editor,
@@ -432,7 +448,14 @@ export function createEditableAPI(
432
448
  options?: EditableAPIDeleteOptions,
433
449
  ): void => {
434
450
  if (selection) {
435
- const range = toSlateRange(selection, editor)
451
+ const range = toSlateRange({
452
+ context: {
453
+ schema: editorActor.getSnapshot().context.schema,
454
+ value: editor.value,
455
+ selection,
456
+ },
457
+ blockIndexMap: editor.blockIndexMap,
458
+ })
436
459
  const hasRange =
437
460
  range && range.anchor.path.length > 0 && range.focus.path.length > 0
438
461
  if (!hasRange) {
@@ -481,8 +504,24 @@ export function createEditableAPI(
481
504
  // that would insert the placeholder into the actual value
482
505
  // which should remain empty)
483
506
  if (editor.children.length === 0) {
484
- editor.children = [editor.pteCreateTextBlock({decorators: []})]
507
+ const placeholderBlock = createPlaceholderBlock(
508
+ editorActor.getSnapshot().context,
509
+ )
510
+ editor.children = [placeholderBlock]
511
+ editor.value = [placeholderBlock]
512
+
513
+ buildIndexMaps(
514
+ {
515
+ schema: editorActor.getSnapshot().context.schema,
516
+ value: editor.value,
517
+ },
518
+ {
519
+ blockIndexMap: editor.blockIndexMap,
520
+ listIndexMap: editor.listIndexMap,
521
+ },
522
+ )
485
523
  }
524
+
486
525
  editor.onChange()
487
526
  }
488
527
  }
@@ -540,8 +579,22 @@ export function createEditableAPI(
540
579
  selectionB: EditorSelection,
541
580
  ) => {
542
581
  // Convert the selections to Slate ranges
543
- const rangeA = toSlateRange(selectionA, editor)
544
- const rangeB = toSlateRange(selectionB, editor)
582
+ const rangeA = toSlateRange({
583
+ context: {
584
+ schema: editorActor.getSnapshot().context.schema,
585
+ value: editor.value,
586
+ selection: selectionA,
587
+ },
588
+ blockIndexMap: editor.blockIndexMap,
589
+ })
590
+ const rangeB = toSlateRange({
591
+ context: {
592
+ schema: editorActor.getSnapshot().context.schema,
593
+ value: editor.value,
594
+ selection: selectionB,
595
+ },
596
+ blockIndexMap: editor.blockIndexMap,
597
+ })
545
598
 
546
599
  // Make sure the ranges are valid
547
600
  const isValidRanges = Range.isRange(rangeA) && Range.isRange(rangeB)
@@ -91,10 +91,14 @@ export const rangeDecorationsMachine = setup({
91
91
  const rangeDecorationState: Array<DecoratedRange> = []
92
92
 
93
93
  for (const rangeDecoration of context.pendingRangeDecorations) {
94
- const slateRange = toSlateRange(
95
- rangeDecoration.selection,
96
- context.slateEditor,
97
- )
94
+ const slateRange = toSlateRange({
95
+ context: {
96
+ schema: context.schema,
97
+ value: context.slateEditor.value,
98
+ selection: rangeDecoration.selection,
99
+ },
100
+ blockIndexMap: context.slateEditor.blockIndexMap,
101
+ })
98
102
 
99
103
  if (!Range.isRange(slateRange)) {
100
104
  rangeDecoration.onMoved?.({
@@ -121,10 +125,14 @@ export const rangeDecorationsMachine = setup({
121
125
  const rangeDecorationState: Array<DecoratedRange> = []
122
126
 
123
127
  for (const rangeDecoration of event.rangeDecorations) {
124
- const slateRange = toSlateRange(
125
- rangeDecoration.selection,
126
- context.slateEditor,
127
- )
128
+ const slateRange = toSlateRange({
129
+ context: {
130
+ schema: context.schema,
131
+ value: context.slateEditor.value,
132
+ selection: rangeDecoration.selection,
133
+ },
134
+ blockIndexMap: context.slateEditor.blockIndexMap,
135
+ })
128
136
 
129
137
  if (!Range.isRange(slateRange)) {
130
138
  rangeDecoration.onMoved?.({
@@ -152,10 +160,14 @@ export const rangeDecorationsMachine = setup({
152
160
  const rangeDecorationState: Array<DecoratedRange> = []
153
161
 
154
162
  for (const decoratedRange of context.slateEditor.decoratedRanges) {
155
- const slateRange = toSlateRange(
156
- decoratedRange.rangeDecoration.selection,
157
- context.slateEditor,
158
- )
163
+ const slateRange = toSlateRange({
164
+ context: {
165
+ schema: context.schema,
166
+ value: context.slateEditor.value,
167
+ selection: decoratedRange.rangeDecoration.selection,
168
+ },
169
+ blockIndexMap: context.slateEditor.blockIndexMap,
170
+ })
159
171
 
160
172
  if (!Range.isRange(slateRange)) {
161
173
  decoratedRange.rangeDecoration.onMoved?.({
@@ -1,37 +1,67 @@
1
- import {Element, type Editor, type Path} from 'slate'
2
- import type {EditorSelectionPoint} from '..'
1
+ import type {Path} from 'slate'
2
+ import type {
3
+ EditorContext,
4
+ EditorSelectionPoint,
5
+ EditorSnapshot,
6
+ PortableTextBlock,
7
+ PortableTextObject,
8
+ PortableTextSpan,
9
+ } from '..'
3
10
  import {
4
11
  getBlockKeyFromSelectionPoint,
5
12
  getChildKeyFromSelectionPoint,
6
13
  } from '../selection/selection-point'
14
+ import {isSpan, isTextBlock} from './parse-blocks'
7
15
 
8
16
  export function toSlatePath(
17
+ snapshot: {
18
+ context: Pick<EditorContext, 'schema' | 'value'>
19
+ } & Pick<EditorSnapshot, 'blockIndexMap'>,
9
20
  path: EditorSelectionPoint['path'],
10
- editor: Editor,
11
- ): Path {
21
+ ): {
22
+ block: PortableTextBlock | undefined
23
+ child: PortableTextSpan | PortableTextObject | undefined
24
+ path: Path
25
+ } {
12
26
  const blockKey = getBlockKeyFromSelectionPoint({
13
27
  path,
14
28
  offset: 0,
15
29
  })
16
30
 
17
31
  if (!blockKey) {
18
- return []
32
+ return {
33
+ block: undefined,
34
+ child: undefined,
35
+ path: [],
36
+ }
19
37
  }
20
38
 
21
- const blockIndex = editor.blockIndexMap.get(blockKey)
39
+ const blockIndex = snapshot.blockIndexMap.get(blockKey)
22
40
 
23
41
  if (blockIndex === undefined) {
24
- return []
42
+ return {
43
+ block: undefined,
44
+ child: undefined,
45
+ path: [],
46
+ }
25
47
  }
26
48
 
27
- const block = editor.children.at(blockIndex)
49
+ const block = snapshot.context.value.at(blockIndex)
28
50
 
29
- if (!block || !Element.isElement(block)) {
30
- return []
51
+ if (!block) {
52
+ return {
53
+ block: undefined,
54
+ child: undefined,
55
+ path: [],
56
+ }
31
57
  }
32
58
 
33
- if (editor.isVoid(block)) {
34
- return [blockIndex, 0]
59
+ if (!isTextBlock(snapshot.context, block)) {
60
+ return {
61
+ block,
62
+ child: undefined,
63
+ path: [blockIndex, 0],
64
+ }
35
65
  }
36
66
 
37
67
  const childKey = getChildKeyFromSelectionPoint({
@@ -40,23 +70,41 @@ export function toSlatePath(
40
70
  })
41
71
 
42
72
  if (!childKey) {
43
- return [blockIndex, 0]
73
+ return {
74
+ block,
75
+ child: undefined,
76
+ path: [blockIndex, 0],
77
+ }
44
78
  }
45
79
 
46
80
  let childPath: Array<number> = []
47
81
  let childIndex = -1
82
+ let pathChild: PortableTextSpan | PortableTextObject | undefined = undefined
48
83
 
49
84
  for (const child of block.children) {
50
85
  childIndex++
51
86
  if (child._key === childKey) {
52
- if (Element.isElement(child) && editor.isVoid(child)) {
53
- childPath = [childIndex, 0]
54
- } else {
87
+ pathChild = child
88
+ if (isSpan(snapshot.context, child)) {
55
89
  childPath = [childIndex]
90
+ } else {
91
+ childPath = [childIndex, 0]
56
92
  }
57
93
  break
58
94
  }
59
95
  }
60
96
 
61
- return [blockIndex].concat(childPath)
97
+ if (childPath.length === 0) {
98
+ return {
99
+ block,
100
+ child: undefined,
101
+ path: [blockIndex, 0],
102
+ }
103
+ }
104
+
105
+ return {
106
+ block,
107
+ child: pathChild,
108
+ path: [blockIndex].concat(childPath),
109
+ }
62
110
  }
@@ -0,0 +1,179 @@
1
+ import {describe, expect, test} from 'vitest'
2
+ import {compileSchemaDefinition} from '../editor/editor-schema'
3
+ import {defineSchema} from '../editor/editor-schema-definition'
4
+ import {toSlateRange} from './ranges'
5
+ import {createTestKeyGenerator} from './test-key-generator'
6
+
7
+ describe(toSlateRange.name, () => {
8
+ const schema = compileSchemaDefinition(
9
+ defineSchema({
10
+ blockObjects: [{name: 'image'}],
11
+ inlineObjects: [
12
+ {name: 'stock-ticker', fields: [{name: 'symbol', type: 'string'}]},
13
+ ],
14
+ }),
15
+ )
16
+
17
+ test("Scenario: Block object offset that doesn't exist", () => {
18
+ const keyGenerator = createTestKeyGenerator()
19
+ const blockObjectKey = keyGenerator()
20
+
21
+ const range = toSlateRange({
22
+ context: {
23
+ schema,
24
+ value: [
25
+ {
26
+ _key: blockObjectKey,
27
+ _type: 'image',
28
+ },
29
+ ],
30
+ selection: {
31
+ anchor: {
32
+ path: [{_key: blockObjectKey}],
33
+ offset: 3,
34
+ },
35
+ focus: {
36
+ path: [{_key: blockObjectKey}],
37
+ offset: 3,
38
+ },
39
+ },
40
+ },
41
+ blockIndexMap: new Map([[blockObjectKey, 0]]),
42
+ })
43
+
44
+ expect(range).toEqual({
45
+ anchor: {path: [0, 0], offset: 0},
46
+ focus: {path: [0, 0], offset: 0},
47
+ })
48
+ })
49
+
50
+ test("Scenario: Child that doesn't exist", () => {
51
+ const keyGenerator = createTestKeyGenerator()
52
+
53
+ const blockKey = keyGenerator()
54
+ const removedChildKey = keyGenerator()
55
+
56
+ const range = toSlateRange({
57
+ context: {
58
+ schema,
59
+ value: [
60
+ {
61
+ _key: blockKey,
62
+ _type: 'block',
63
+ children: [
64
+ {
65
+ _key: keyGenerator(),
66
+ _type: 'span',
67
+ text: 'foobar',
68
+ },
69
+ ],
70
+ },
71
+ ],
72
+ selection: {
73
+ anchor: {
74
+ path: [{_key: blockKey}, 'children', {_key: removedChildKey}],
75
+ offset: 3,
76
+ },
77
+ focus: {
78
+ path: [{_key: blockKey}, 'children', {_key: removedChildKey}],
79
+ offset: 3,
80
+ },
81
+ },
82
+ },
83
+ blockIndexMap: new Map([[blockKey, 0]]),
84
+ })
85
+
86
+ expect(range).toEqual({
87
+ anchor: {path: [0, 0], offset: 0},
88
+ focus: {path: [0, 0], offset: 0},
89
+ })
90
+ })
91
+
92
+ test("Scenario: Span offset that doesn't exist", () => {
93
+ const keyGenerator = createTestKeyGenerator()
94
+ const blockKey = keyGenerator()
95
+ const spanKey = keyGenerator()
96
+
97
+ const range = toSlateRange({
98
+ context: {
99
+ schema,
100
+ value: [
101
+ {
102
+ _key: blockKey,
103
+ _type: 'block',
104
+ children: [
105
+ {
106
+ _key: spanKey,
107
+ _type: 'span',
108
+ text: 'foo',
109
+ },
110
+ ],
111
+ },
112
+ ],
113
+ selection: {
114
+ anchor: {
115
+ path: [{_key: blockKey}, 'children', {_key: spanKey}],
116
+ offset: 4,
117
+ },
118
+ focus: {
119
+ path: [{_key: blockKey}, 'children', {_key: spanKey}],
120
+ offset: 4,
121
+ },
122
+ },
123
+ },
124
+ blockIndexMap: new Map([[blockKey, 0]]),
125
+ })
126
+
127
+ expect(range).toEqual({
128
+ anchor: {path: [0, 0], offset: 3},
129
+ focus: {path: [0, 0], offset: 3},
130
+ })
131
+ })
132
+
133
+ test("Scenario: Inline object offset that doesn't exist", () => {
134
+ const keyGenerator = createTestKeyGenerator()
135
+ const blockKey = keyGenerator()
136
+ const inlineObjectKey = keyGenerator()
137
+
138
+ const range = toSlateRange({
139
+ context: {
140
+ schema,
141
+ value: [
142
+ {
143
+ _key: blockKey,
144
+ _type: 'block',
145
+ children: [
146
+ {_key: keyGenerator(), _type: 'span', text: 'foo'},
147
+ {
148
+ _key: inlineObjectKey,
149
+ _type: 'stock-ticker',
150
+ symbol: 'AAPL',
151
+ },
152
+ {
153
+ _key: keyGenerator(),
154
+ _type: 'span',
155
+ text: 'bar',
156
+ },
157
+ ],
158
+ },
159
+ ],
160
+ selection: {
161
+ anchor: {
162
+ path: [{_key: blockKey}, 'children', {_key: inlineObjectKey}],
163
+ offset: 3,
164
+ },
165
+ focus: {
166
+ path: [{_key: blockKey}, 'children', {_key: inlineObjectKey}],
167
+ offset: 3,
168
+ },
169
+ },
170
+ },
171
+ blockIndexMap: new Map([[blockKey, 0]]),
172
+ })
173
+
174
+ expect(range).toEqual({
175
+ anchor: {path: [0, 1, 0], offset: 0},
176
+ focus: {path: [0, 1, 0], offset: 0},
177
+ })
178
+ })
179
+ })
@@ -1,37 +1,54 @@
1
- import {Point, type Editor, type Operation, type Range} from 'slate'
2
- import type {EditorSelection} from '../types/editor'
1
+ import {Point, type Operation, type Range} from 'slate'
2
+ import type {EditorContext, EditorSnapshot} from '../editor/editor-snapshot'
3
+ import {isSpan} from './parse-blocks'
3
4
  import {toSlatePath} from './paths'
4
5
 
5
- export interface ObjectWithKeyAndType {
6
- _key: string
7
- _type: string
8
- children?: ObjectWithKeyAndType[]
9
- }
10
-
11
6
  export function toSlateRange(
12
- selection: EditorSelection,
13
- editor: Editor,
7
+ snapshot: {
8
+ context: Pick<EditorContext, 'schema' | 'value' | 'selection'>
9
+ } & Pick<EditorSnapshot, 'blockIndexMap'>,
14
10
  ): Range | null {
15
- if (!selection || !editor) {
11
+ if (!snapshot.context.selection) {
16
12
  return null
17
13
  }
18
14
 
19
- const anchor = {
20
- path: toSlatePath(selection.anchor.path, editor),
21
- offset: selection.anchor.offset,
22
- }
23
- const focus = {
24
- path: toSlatePath(selection.focus.path, editor),
25
- offset: selection.focus.offset,
26
- }
15
+ const anchorPath = toSlatePath(
16
+ snapshot,
17
+ snapshot.context.selection.anchor.path,
18
+ )
19
+ const focusPath = toSlatePath(snapshot, snapshot.context.selection.focus.path)
27
20
 
28
- if (focus.path.length === 0 || anchor.path.length === 0) {
21
+ if (anchorPath.path.length === 0 || focusPath.path.length === 0) {
29
22
  return null
30
23
  }
31
24
 
32
- const range = anchor && focus ? {anchor, focus} : null
25
+ const anchorOffset = anchorPath.child
26
+ ? isSpan(snapshot.context, anchorPath.child)
27
+ ? Math.min(
28
+ anchorPath.child.text.length,
29
+ snapshot.context.selection.anchor.offset,
30
+ )
31
+ : 0
32
+ : 0
33
+ const focusOffset = focusPath.child
34
+ ? isSpan(snapshot.context, focusPath.child)
35
+ ? Math.min(
36
+ focusPath.child.text.length,
37
+ snapshot.context.selection.focus.offset,
38
+ )
39
+ : 0
40
+ : 0
33
41
 
34
- return range
42
+ return {
43
+ anchor: {
44
+ path: anchorPath.path,
45
+ offset: anchorOffset,
46
+ },
47
+ focus: {
48
+ path: focusPath.path,
49
+ offset: focusOffset,
50
+ },
51
+ }
35
52
  }
36
53
 
37
54
  export function moveRangeByOperation(