@portabletext/editor 1.11.3 → 1.12.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.
@@ -1,4 +1,8 @@
1
- import type {KeyedSegment, PortableTextBlock} from '@sanity/types'
1
+ import type {
2
+ KeyedSegment,
3
+ PortableTextBlock,
4
+ PortableTextTextBlock,
5
+ } from '@sanity/types'
2
6
  import type {TextUnit} from 'slate'
3
7
  import type {TextInsertTextOptions} from 'slate/dist/interfaces/transforms/text'
4
8
  import type {
@@ -6,6 +10,7 @@ import type {
6
10
  PortableTextMemberSchemaTypes,
7
11
  PortableTextSlateEditor,
8
12
  } from '../../types/editor'
13
+ import type {BlockOffset} from './behavior.utils.block-offset'
9
14
 
10
15
  /**
11
16
  * @alpha
@@ -100,8 +105,11 @@ export type BehaviorActionIntend =
100
105
  | BehaviorEvent
101
106
  | {
102
107
  type: 'insert block object'
103
- name: string
104
- value?: {[prop: string]: unknown}
108
+ placement: 'auto' | 'after'
109
+ blockObject: {
110
+ name: string
111
+ value?: {[prop: string]: unknown}
112
+ }
105
113
  }
106
114
  | {
107
115
  type: 'insert span'
@@ -114,7 +122,10 @@ export type BehaviorActionIntend =
114
122
  }
115
123
  | {
116
124
  type: 'insert text block'
117
- decorators: Array<string>
125
+ placement: 'auto' | 'after'
126
+ textBlock?: {
127
+ children?: PortableTextTextBlock['children']
128
+ }
118
129
  }
119
130
  | {
120
131
  type: 'set block'
@@ -129,8 +140,13 @@ export type BehaviorActionIntend =
129
140
  props: Array<'style' | 'listItem' | 'level'>
130
141
  }
131
142
  | {
132
- type: 'delete'
133
- selection: NonNullable<EditorSelection>
143
+ type: 'delete block'
144
+ blockPath: [KeyedSegment]
145
+ }
146
+ | {
147
+ type: 'delete text'
148
+ anchor: BlockOffset
149
+ focus: BlockOffset
134
150
  }
135
151
  | {
136
152
  type: 'effect'
@@ -0,0 +1,143 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import {expect, test} from 'vitest'
3
+ import {blockOffsetToSpanSelectionPoint} from './behavior.utils.block-offset'
4
+
5
+ test(blockOffsetToSpanSelectionPoint.name, () => {
6
+ const value: Array<PortableTextBlock> = [
7
+ {
8
+ _key: 'b1',
9
+ _type: 'image',
10
+ },
11
+ {
12
+ _key: 'b2',
13
+ _type: 'block',
14
+ children: [
15
+ {
16
+ _key: 's1',
17
+ _type: 'span',
18
+ text: 'Hello, ',
19
+ },
20
+ {
21
+ _key: 's2',
22
+ _type: 'span',
23
+ text: 'world!',
24
+ marks: ['strong'],
25
+ },
26
+ ],
27
+ },
28
+ {
29
+ _key: 'b3',
30
+ _type: 'block',
31
+ children: [
32
+ {
33
+ _key: 's3',
34
+ _type: 'span',
35
+ text: 'Here is a ',
36
+ },
37
+ {
38
+ _key: 's4',
39
+ _type: 'stock-ticker',
40
+ },
41
+ {
42
+ _key: 's5',
43
+ _type: 'span',
44
+ text: '.',
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ _key: 'b4',
50
+ _type: 'block',
51
+ children: [
52
+ {
53
+ _key: 's6',
54
+ _type: 'stock-ticker',
55
+ },
56
+ {
57
+ _key: 's7',
58
+ _type: 'stock-ticker',
59
+ },
60
+ {
61
+ _key: 's8',
62
+ _type: 'stock-ticker',
63
+ },
64
+ ],
65
+ },
66
+ ]
67
+
68
+ expect(
69
+ blockOffsetToSpanSelectionPoint({
70
+ value,
71
+ blockOffset: {
72
+ path: [{_key: 'b1'}],
73
+ offset: 0,
74
+ },
75
+ }),
76
+ ).toBeUndefined()
77
+ expect(
78
+ blockOffsetToSpanSelectionPoint({
79
+ value,
80
+ blockOffset: {
81
+ path: [{_key: 'b2'}],
82
+ offset: 9,
83
+ },
84
+ }),
85
+ ).toEqual({
86
+ path: [{_key: 'b2'}, 'children', {_key: 's2'}],
87
+ offset: 2,
88
+ })
89
+ expect(
90
+ blockOffsetToSpanSelectionPoint({
91
+ value,
92
+ blockOffset: {
93
+ path: [{_key: 'b3'}],
94
+ offset: 9,
95
+ },
96
+ }),
97
+ ).toEqual({
98
+ path: [{_key: 'b3'}, 'children', {_key: 's3'}],
99
+ offset: 9,
100
+ })
101
+ expect(
102
+ blockOffsetToSpanSelectionPoint({
103
+ value,
104
+ blockOffset: {
105
+ path: [{_key: 'b3'}],
106
+ offset: 10,
107
+ },
108
+ }),
109
+ ).toEqual({
110
+ path: [{_key: 'b3'}, 'children', {_key: 's3'}],
111
+ offset: 10,
112
+ })
113
+ expect(
114
+ blockOffsetToSpanSelectionPoint({
115
+ value,
116
+ blockOffset: {
117
+ path: [{_key: 'b3'}],
118
+ offset: 11,
119
+ },
120
+ }),
121
+ ).toEqual({
122
+ path: [{_key: 'b3'}, 'children', {_key: 's5'}],
123
+ offset: 1,
124
+ })
125
+ expect(
126
+ blockOffsetToSpanSelectionPoint({
127
+ value,
128
+ blockOffset: {
129
+ path: [{_key: 'b4'}],
130
+ offset: 0,
131
+ },
132
+ }),
133
+ ).toBeUndefined()
134
+ expect(
135
+ blockOffsetToSpanSelectionPoint({
136
+ value,
137
+ blockOffset: {
138
+ path: [{_key: 'b4'}],
139
+ offset: 1,
140
+ },
141
+ }),
142
+ ).toBeUndefined()
143
+ })
@@ -0,0 +1,101 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ isPortableTextTextBlock,
4
+ type KeyedSegment,
5
+ type PortableTextBlock,
6
+ } from '@sanity/types'
7
+
8
+ /**
9
+ * @alpha
10
+ */
11
+ export type BlockOffset = {
12
+ path: [KeyedSegment]
13
+ offset: number
14
+ }
15
+
16
+ export function blockOffsetToSpanSelectionPoint({
17
+ value,
18
+ blockOffset,
19
+ }: {
20
+ value: Array<PortableTextBlock>
21
+ blockOffset: BlockOffset
22
+ }) {
23
+ let offsetLeft = blockOffset.offset
24
+ let selectionPoint:
25
+ | {path: [KeyedSegment, 'children', KeyedSegment]; offset: number}
26
+ | undefined
27
+
28
+ for (const block of value) {
29
+ if (block._key !== blockOffset.path[0]._key) {
30
+ continue
31
+ }
32
+
33
+ if (!isPortableTextTextBlock(block)) {
34
+ continue
35
+ }
36
+
37
+ for (const child of block.children) {
38
+ if (!isPortableTextSpan(child)) {
39
+ continue
40
+ }
41
+
42
+ if (offsetLeft === 0) {
43
+ selectionPoint = {
44
+ path: [...blockOffset.path, 'children', {_key: child._key}],
45
+ offset: 0,
46
+ }
47
+ break
48
+ }
49
+
50
+ if (offsetLeft <= child.text.length) {
51
+ selectionPoint = {
52
+ path: [...blockOffset.path, 'children', {_key: child._key}],
53
+ offset: offsetLeft,
54
+ }
55
+ break
56
+ }
57
+
58
+ offsetLeft -= child.text.length
59
+ }
60
+ }
61
+
62
+ return selectionPoint
63
+ }
64
+
65
+ export function spanSelectionPointToBlockOffset({
66
+ value,
67
+ selectionPoint,
68
+ }: {
69
+ value: Array<PortableTextBlock>
70
+ selectionPoint: {
71
+ path: [KeyedSegment, 'children', KeyedSegment]
72
+ offset: number
73
+ }
74
+ }): BlockOffset | undefined {
75
+ let offset = 0
76
+
77
+ for (const block of value) {
78
+ if (block._key !== selectionPoint.path[0]._key) {
79
+ continue
80
+ }
81
+
82
+ if (!isPortableTextTextBlock(block)) {
83
+ continue
84
+ }
85
+
86
+ for (const child of block.children) {
87
+ if (!isPortableTextSpan(child)) {
88
+ continue
89
+ }
90
+
91
+ if (child._key === selectionPoint.path[2]._key) {
92
+ return {
93
+ path: [{_key: block._key}],
94
+ offset: offset + selectionPoint.offset,
95
+ }
96
+ }
97
+
98
+ offset += child.text.length
99
+ }
100
+ }
101
+ }
@@ -202,6 +202,17 @@ export function getNextBlock(
202
202
  return undefined
203
203
  }
204
204
 
205
- export function isEmptyTextBlock(block: PortableTextTextBlock) {
206
- return block.children.length === 1 && block.children[0].text === ''
205
+ export function isEmptyTextBlock(block: PortableTextBlock) {
206
+ if (!isPortableTextTextBlock(block)) {
207
+ return false
208
+ }
209
+
210
+ const onlyText = block.children.every(isPortableTextSpan)
211
+ const blockText = getTextBlockText(block)
212
+
213
+ return onlyText && blockText === ''
214
+ }
215
+
216
+ export function getTextBlockText(block: PortableTextTextBlock) {
217
+ return block.children.map((child) => child.text ?? '').join('')
207
218
  }
@@ -26,15 +26,12 @@ import type {
26
26
  } from '../../types/editor'
27
27
  import {debugWithName} from '../../utils/debug'
28
28
  import {toPortableTextRange, toSlateRange} from '../../utils/ranges'
29
- import {
30
- fromSlateValue,
31
- isEqualToEmptyEditor,
32
- toSlateValue,
33
- } from '../../utils/values'
29
+ import {fromSlateValue, toSlateValue} from '../../utils/values'
34
30
  import {
35
31
  KEY_TO_VALUE_ELEMENT,
36
32
  SLATE_TO_PORTABLE_TEXT_RANGE,
37
33
  } from '../../utils/weakMaps'
34
+ import {insertBlockObjectActionImplementation} from '../behavior/behavior.action.insert-block-object'
38
35
  import type {BehaviorActionImplementation} from '../behavior/behavior.actions'
39
36
  import type {EditorActor} from '../editor-machine'
40
37
  import {isDecoratorActive} from './createWithPortableTextMarkModel'
@@ -206,18 +203,35 @@ export function createEditableAPI(
206
203
  type: TSchemaType,
207
204
  value?: {[prop: string]: any},
208
205
  ): Path => {
209
- return insertBlockObjectActionImplementation({
206
+ insertBlockObjectActionImplementation({
210
207
  context: {
211
208
  keyGenerator: editorActor.getSnapshot().context.keyGenerator,
212
209
  schema: types,
213
210
  },
214
211
  action: {
215
212
  type: 'insert block object',
216
- name: type.name,
217
- value,
213
+ blockObject: {
214
+ name: type.name,
215
+ value,
216
+ },
217
+ placement: 'auto',
218
218
  editor,
219
219
  },
220
220
  })
221
+
222
+ editor.onChange()
223
+
224
+ return (
225
+ toPortableTextRange(
226
+ fromSlateValue(
227
+ editor.children,
228
+ types.block.name,
229
+ KEY_TO_VALUE_ELEMENT.get(editor),
230
+ ),
231
+ editor.selection,
232
+ types,
233
+ )?.focus.path ?? []
234
+ )
221
235
  },
222
236
  hasBlockStyle: (style: string): boolean => {
223
237
  try {
@@ -487,85 +501,6 @@ export function createEditableAPI(
487
501
  return editableApi
488
502
  }
489
503
 
490
- export const insertBlockObjectActionImplementation: BehaviorActionImplementation<
491
- 'insert block object',
492
- Path
493
- > = ({context, action}) => {
494
- const editor = action.editor
495
- const types = context.schema
496
- const block = toSlateValue(
497
- [
498
- {
499
- _key: context.keyGenerator(),
500
- _type: action.name,
501
- ...(action.value ? action.value : {}),
502
- },
503
- ],
504
- {schemaTypes: context.schema},
505
- )[0] as unknown as Node
506
-
507
- if (!editor.selection) {
508
- const lastBlock = Array.from(
509
- Editor.nodes(editor, {
510
- match: (n) => !Editor.isEditor(n),
511
- at: [],
512
- reverse: true,
513
- }),
514
- )[0]
515
-
516
- // If there is no selection, let's just insert the new block at the
517
- // end of the document
518
- Editor.insertNode(editor, block)
519
-
520
- if (lastBlock && isEqualToEmptyEditor([lastBlock[0]], types)) {
521
- // And if the last block was an empty text block, let's remove
522
- // that too
523
- Transforms.removeNodes(editor, {at: lastBlock[1]})
524
- }
525
-
526
- editor.onChange()
527
-
528
- return (
529
- toPortableTextRange(
530
- fromSlateValue(
531
- editor.children,
532
- types.block.name,
533
- KEY_TO_VALUE_ELEMENT.get(editor),
534
- ),
535
- editor.selection,
536
- types,
537
- )?.focus.path ?? []
538
- )
539
- }
540
-
541
- const focusBlock = Array.from(
542
- Editor.nodes(editor, {
543
- at: editor.selection.focus.path.slice(0, 1),
544
- match: (n) => n._type === types.block.name,
545
- }),
546
- )[0]
547
-
548
- Editor.insertNode(editor, block)
549
-
550
- if (focusBlock && isEqualToEmptyEditor([focusBlock[0]], types)) {
551
- Transforms.removeNodes(editor, {at: focusBlock[1]})
552
- }
553
-
554
- editor.onChange()
555
-
556
- return (
557
- toPortableTextRange(
558
- fromSlateValue(
559
- editor.children,
560
- types.block.name,
561
- KEY_TO_VALUE_ELEMENT.get(editor),
562
- ),
563
- editor.selection,
564
- types,
565
- )?.focus.path || []
566
- )
567
- }
568
-
569
504
  function isAnnotationActive({
570
505
  editor,
571
506
  annotation,
package/src/index.ts CHANGED
@@ -20,6 +20,7 @@ export {
20
20
  type OmitFromUnion,
21
21
  type PickFromUnion,
22
22
  } from './editor/behavior/behavior.types'
23
+ export type {BlockOffset} from './editor/behavior/behavior.utils.block-offset'
23
24
  export type {SlateEditor} from './editor/create-slate-editor'
24
25
  export {
25
26
  defineSchema,