@portabletext/editor 1.36.5 → 1.37.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 (46) hide show
  1. package/lib/_chunks-cjs/behavior.core.cjs.map +1 -1
  2. package/lib/_chunks-cjs/behavior.markdown.cjs +1 -1
  3. package/lib/_chunks-cjs/editor-provider.cjs +104 -7
  4. package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
  5. package/lib/_chunks-cjs/util.block-offsets-to-selection.cjs.map +1 -1
  6. package/lib/_chunks-es/behavior.core.js.map +1 -1
  7. package/lib/_chunks-es/behavior.markdown.js +1 -1
  8. package/lib/_chunks-es/editor-provider.js +108 -11
  9. package/lib/_chunks-es/editor-provider.js.map +1 -1
  10. package/lib/_chunks-es/util.block-offsets-to-selection.js.map +1 -1
  11. package/lib/behaviors/index.d.cts +59 -0
  12. package/lib/behaviors/index.d.ts +59 -0
  13. package/lib/index.d.cts +59 -0
  14. package/lib/index.d.ts +59 -0
  15. package/lib/plugins/index.cjs +1 -1
  16. package/lib/plugins/index.d.cts +59 -0
  17. package/lib/plugins/index.d.ts +59 -0
  18. package/lib/plugins/index.js +1 -1
  19. package/lib/selectors/index.cjs +69 -14
  20. package/lib/selectors/index.cjs.map +1 -1
  21. package/lib/selectors/index.d.cts +75 -0
  22. package/lib/selectors/index.d.ts +75 -0
  23. package/lib/selectors/index.js +63 -8
  24. package/lib/selectors/index.js.map +1 -1
  25. package/lib/utils/index.d.cts +59 -0
  26. package/lib/utils/index.d.ts +59 -0
  27. package/package.json +7 -7
  28. package/src/behavior-actions/behavior.action.decorator.add.ts +1 -0
  29. package/src/behavior-actions/behavior.action.delete.text.ts +1 -0
  30. package/src/behavior-actions/behavior.action.delete.ts +1 -3
  31. package/src/behavior-actions/behavior.action.insert-blocks.ts +98 -2
  32. package/src/behavior-actions/behavior.actions.ts +1 -0
  33. package/src/behaviors/behavior.default.ts +1 -0
  34. package/src/behaviors/behavior.types.ts +1 -0
  35. package/src/editor/editor-machine.ts +16 -3
  36. package/src/editor/editor-selector.ts +1 -0
  37. package/src/editor/editor-snapshot.ts +4 -0
  38. package/src/internal-utils/create-test-snapshot.ts +1 -0
  39. package/src/internal-utils/parse-blocks.ts +22 -0
  40. package/src/selectors/index.ts +2 -0
  41. package/src/selectors/selector.get-focus-inline-object.ts +21 -0
  42. package/src/selectors/selector.is-overlapping-selection.test.ts +171 -0
  43. package/src/selectors/selector.is-overlapping-selection.ts +108 -4
  44. package/src/selectors/selector.is-point-after-selection.ts +3 -1
  45. package/src/selectors/selector.is-point-before-selection.ts +3 -1
  46. package/src/selectors/selector.is-selecting-entire-blocks.ts +34 -0
@@ -360,7 +360,10 @@ export const editorMachine = setup({
360
360
  withApplyingBehaviorActions(event.editor, () => {
361
361
  try {
362
362
  performAction({
363
- context,
363
+ context: {
364
+ keyGenerator: context.keyGenerator,
365
+ schema: context.schema,
366
+ },
364
367
  action: defaultAction,
365
368
  })
366
369
  } catch (error) {
@@ -379,6 +382,7 @@ export const editorMachine = setup({
379
382
  converters: [...context.converters],
380
383
  editor: event.editor,
381
384
  keyGenerator: context.keyGenerator,
385
+ readOnly: self.getSnapshot().matches({'edit mode': 'read only'}),
382
386
  schema: context.schema,
383
387
  hasTag: (tag) => self.getSnapshot().hasTag(tag),
384
388
  })
@@ -440,7 +444,13 @@ export const editorMachine = setup({
440
444
  }
441
445
 
442
446
  try {
443
- performAction({context, action: internalAction})
447
+ performAction({
448
+ context: {
449
+ keyGenerator: context.keyGenerator,
450
+ schema: context.schema,
451
+ },
452
+ action: internalAction,
453
+ })
444
454
  } catch (error) {
445
455
  console.error(
446
456
  new Error(
@@ -483,7 +493,10 @@ export const editorMachine = setup({
483
493
  withApplyingBehaviorActions(event.editor, () => {
484
494
  try {
485
495
  performAction({
486
- context,
496
+ context: {
497
+ keyGenerator: context.keyGenerator,
498
+ schema: context.schema,
499
+ },
487
500
  action: defaultAction,
488
501
  })
489
502
  } catch (error) {
@@ -73,6 +73,7 @@ export function getEditorSnapshot({
73
73
  slateEditorInstance,
74
74
  }),
75
75
  keyGenerator: editorActorSnapshot.context.keyGenerator,
76
+ readOnly: editorActorSnapshot.matches({'edit mode': 'read only'}),
76
77
  schema: editorActorSnapshot.context.schema,
77
78
  selection: editorActorSnapshot.context.selection,
78
79
  value: getValue({editorActorSnapshot, slateEditorInstance}),
@@ -15,6 +15,7 @@ export type EditorContext = {
15
15
  activeDecorators: Array<string>
16
16
  converters: Array<Converter>
17
17
  keyGenerator: () => string
18
+ readOnly: boolean
18
19
  schema: EditorSchema
19
20
  selection: EditorSelection
20
21
  value: Array<PortableTextBlock>
@@ -38,12 +39,14 @@ export function createEditorSnapshot({
38
39
  converters,
39
40
  editor,
40
41
  keyGenerator,
42
+ readOnly,
41
43
  schema,
42
44
  hasTag,
43
45
  }: {
44
46
  converters: Array<Converter>
45
47
  editor: PortableTextSlateEditor
46
48
  keyGenerator: () => string
49
+ readOnly: boolean
47
50
  schema: EditorSchema
48
51
  hasTag: HasTag
49
52
  }) {
@@ -61,6 +64,7 @@ export function createEditorSnapshot({
61
64
  }),
62
65
  converters,
63
66
  keyGenerator,
67
+ readOnly,
64
68
  schema,
65
69
  selection,
66
70
  value,
@@ -13,6 +13,7 @@ export function createTestSnapshot(snapshot: {
13
13
  snapshot.context?.schema ?? compileSchemaDefinition(defineSchema({})),
14
14
  keyGenerator: snapshot.context?.keyGenerator ?? createTestKeyGenerator(),
15
15
  activeDecorators: snapshot.context?.activeDecorators ?? [],
16
+ readOnly: snapshot.context?.readOnly ?? false,
16
17
  value: snapshot.context?.value ?? [],
17
18
  selection: snapshot.context?.selection ?? null,
18
19
  },
@@ -8,6 +8,28 @@ import type {EditorSchema} from '../editor/define-schema'
8
8
  import type {EditorContext} from '../editor/editor-snapshot'
9
9
  import {isTypedObject} from './asserters'
10
10
 
11
+ export function parseBlocks({
12
+ context,
13
+ blocks,
14
+ options,
15
+ }: {
16
+ context: Pick<EditorContext, 'keyGenerator' | 'schema'>
17
+ blocks: unknown
18
+ options: {
19
+ refreshKeys: boolean
20
+ }
21
+ }): Array<PortableTextBlock> {
22
+ if (!Array.isArray(blocks)) {
23
+ return []
24
+ }
25
+
26
+ return blocks.flatMap((block) => {
27
+ const parsedBlock = parseBlock({context, block, options})
28
+
29
+ return parsedBlock ? [parsedBlock] : []
30
+ })
31
+ }
32
+
11
33
  export function parseBlock({
12
34
  context,
13
35
  block,
@@ -1,3 +1,4 @@
1
+ export {isSelectingEntireBlocks} from './selector.is-selecting-entire-blocks'
1
2
  export {getActiveAnnotations} from './selector.get-active-annotations'
2
3
  export {getActiveListItem} from './selector.get-active-list-item'
3
4
  export {getActiveStyle} from './selector.get-active-style'
@@ -7,6 +8,7 @@ export {getAnchorSpan} from './selector.get-anchor-span'
7
8
  export {getAnchorTextBlock} from './selector.get-anchor-text-block'
8
9
  export {getBlockOffsets} from './selector.get-block-offsets'
9
10
  export {getCaretWordSelection} from './selector.get-caret-word-selection'
11
+ export {getFocusInlineObject} from './selector.get-focus-inline-object'
10
12
  export {getNextInlineObject} from './selector.get-next-inline-object'
11
13
  export {getPreviousInlineObject} from './selector.get-previous-inline-object'
12
14
  export {getSelectedSlice} from './selector.get-selected-slice'
@@ -0,0 +1,21 @@
1
+ import {
2
+ isPortableTextSpan,
3
+ type KeyedSegment,
4
+ type PortableTextObject,
5
+ } from '@sanity/types'
6
+ import type {EditorSelector} from '../editor/editor-selector'
7
+ import {getFocusChild} from './selectors'
8
+
9
+ /**
10
+ * @public
11
+ */
12
+ export const getFocusInlineObject: EditorSelector<
13
+ | {node: PortableTextObject; path: [KeyedSegment, 'children', KeyedSegment]}
14
+ | undefined
15
+ > = (snapshot) => {
16
+ const focusChild = getFocusChild(snapshot)
17
+
18
+ return focusChild && !isPortableTextSpan(focusChild.node)
19
+ ? {node: focusChild.node, path: focusChild.path}
20
+ : undefined
21
+ }
@@ -0,0 +1,171 @@
1
+ import {describe, expect, test} from 'vitest'
2
+ import {createTestSnapshot} from '../internal-utils/create-test-snapshot'
3
+ import type {EditorSelection} from '../types/editor'
4
+ import {isOverlappingSelection} from './selector.is-overlapping-selection'
5
+
6
+ function snapshot(selection: EditorSelection) {
7
+ return createTestSnapshot({
8
+ context: {
9
+ selection,
10
+ value: [
11
+ {_type: 'image', _key: 'k0'},
12
+ {
13
+ _type: 'block',
14
+ _key: 'k1',
15
+ children: [
16
+ {_type: 'span', _key: 'k3', text: 'foo'},
17
+ {_type: 'stock-ticker', _key: 'k4'},
18
+ {_type: 'span', _key: 'k5', text: 'bar'},
19
+ ],
20
+ },
21
+ {_type: 'image', _key: 'k2'},
22
+ ],
23
+ },
24
+ })
25
+ }
26
+
27
+ describe(isOverlappingSelection.name, () => {
28
+ test('null', () => {
29
+ expect(isOverlappingSelection(null)(snapshot(null))).toBe(false)
30
+ })
31
+
32
+ test('fully selected block object', () => {
33
+ expect(
34
+ isOverlappingSelection({
35
+ anchor: {path: [{_key: 'k0'}], offset: 0},
36
+ focus: {path: [{_key: 'k0'}], offset: 0},
37
+ })(
38
+ snapshot({
39
+ anchor: {path: [{_key: 'k0'}], offset: 0},
40
+ focus: {path: [{_key: 'k0'}], offset: 0},
41
+ }),
42
+ ),
43
+ ).toBe(true)
44
+ })
45
+
46
+ test('block object inside selection', () => {
47
+ expect(
48
+ isOverlappingSelection({
49
+ anchor: {path: [{_key: 'k0'}], offset: 0},
50
+ focus: {path: [{_key: 'k0'}], offset: 0},
51
+ })(
52
+ snapshot({
53
+ anchor: {path: [{_key: 'k0'}], offset: 0},
54
+ focus: {path: [{_key: 'k2'}], offset: 0},
55
+ }),
56
+ ),
57
+ ).toBe(true)
58
+ })
59
+
60
+ test('fully selected inline object', () => {
61
+ expect(
62
+ isOverlappingSelection({
63
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
64
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
65
+ })(
66
+ snapshot({
67
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
68
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
69
+ }),
70
+ ),
71
+ ).toBe(true)
72
+ })
73
+
74
+ test('inline object inside selection', () => {
75
+ expect(
76
+ isOverlappingSelection({
77
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
78
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
79
+ })(
80
+ snapshot({
81
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
82
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
83
+ }),
84
+ ),
85
+ ).toBe(true)
86
+ })
87
+
88
+ test('selection right before', () => {
89
+ expect(
90
+ isOverlappingSelection({
91
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 0},
92
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
93
+ })(
94
+ snapshot({
95
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
96
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
97
+ }),
98
+ ),
99
+ ).toBe(false)
100
+ })
101
+
102
+ test('selection overlapping from the start', () => {
103
+ expect(
104
+ isOverlappingSelection({
105
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 0},
106
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 3},
107
+ })(
108
+ snapshot({
109
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
110
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
111
+ }),
112
+ ),
113
+ ).toBe(true)
114
+ })
115
+
116
+ test('selection right after', () => {
117
+ expect(
118
+ isOverlappingSelection({
119
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
120
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 2},
121
+ })(
122
+ snapshot({
123
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
124
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
125
+ }),
126
+ ),
127
+ ).toBe(false)
128
+ })
129
+
130
+ test('selection overlapping from the end', () => {
131
+ expect(
132
+ isOverlappingSelection({
133
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 0},
134
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 2},
135
+ })(
136
+ snapshot({
137
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
138
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 1},
139
+ }),
140
+ ),
141
+ ).toBe(true)
142
+ })
143
+
144
+ test('before inline object', () => {
145
+ expect(
146
+ isOverlappingSelection({
147
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
148
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k3'}], offset: 2},
149
+ })(
150
+ snapshot({
151
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
152
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
153
+ }),
154
+ ),
155
+ ).toBe(false)
156
+ })
157
+
158
+ test('after inline object', () => {
159
+ expect(
160
+ isOverlappingSelection({
161
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 2},
162
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k5'}], offset: 2},
163
+ })(
164
+ snapshot({
165
+ anchor: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
166
+ focus: {path: [{_key: 'k1'}, 'children', {_key: 'k4'}], offset: 0},
167
+ }),
168
+ ),
169
+ ).toBe(false)
170
+ })
171
+ })
@@ -1,4 +1,5 @@
1
1
  import type {EditorSelection} from '../types/editor'
2
+ import {isEqualSelectionPoints} from '../utils'
2
3
  import type {EditorSelector} from './../editor/editor-selector'
3
4
  import {getSelectionEndPoint} from './selector.get-selection-end-point'
4
5
  import {getSelectionStartPoint} from './selector.get-selection-start-point'
@@ -31,18 +32,121 @@ export function isOverlappingSelection(
31
32
  },
32
33
  })
33
34
 
34
- if (!selectionStartPoint || !selectionEndPoint) {
35
+ const originalSelectionStartPoint = getSelectionStartPoint(snapshot)
36
+ const originalSelectionEndPoint = getSelectionEndPoint(snapshot)
37
+
38
+ if (
39
+ !selectionStartPoint ||
40
+ !selectionEndPoint ||
41
+ !originalSelectionStartPoint ||
42
+ !originalSelectionEndPoint
43
+ ) {
35
44
  return false
36
45
  }
37
46
 
38
- if (!isPointAfterSelection(selectionStartPoint)(snapshot)) {
47
+ const startPointBeforeSelection =
48
+ isPointBeforeSelection(selectionStartPoint)(snapshot)
49
+ const startPointAfterSelection =
50
+ isPointAfterSelection(selectionStartPoint)(snapshot)
51
+ const endPointBeforeSelection =
52
+ isPointBeforeSelection(selectionEndPoint)(snapshot)
53
+ const endPointAfterSelection =
54
+ isPointAfterSelection(selectionEndPoint)(snapshot)
55
+
56
+ const originalStartPointBeforeStartPoint = isPointBeforeSelection(
57
+ originalSelectionStartPoint,
58
+ )({
59
+ ...snapshot,
60
+ context: {
61
+ ...snapshot.context,
62
+ selection: {
63
+ anchor: selectionStartPoint,
64
+ focus: selectionStartPoint,
65
+ },
66
+ },
67
+ })
68
+ const originalStartPointAfterStartPoint = isPointAfterSelection(
69
+ originalSelectionStartPoint,
70
+ )({
71
+ ...snapshot,
72
+ context: {
73
+ ...snapshot.context,
74
+ selection: {
75
+ anchor: selectionStartPoint,
76
+ focus: selectionStartPoint,
77
+ },
78
+ },
79
+ })
80
+
81
+ const originalEndPointBeforeEndPoint = isPointBeforeSelection(
82
+ originalSelectionEndPoint,
83
+ )({
84
+ ...snapshot,
85
+ context: {
86
+ ...snapshot.context,
87
+ selection: {
88
+ anchor: selectionEndPoint,
89
+ focus: selectionEndPoint,
90
+ },
91
+ },
92
+ })
93
+ const originalEndPointAfterEndPoint = isPointAfterSelection(
94
+ originalSelectionEndPoint,
95
+ )({
96
+ ...snapshot,
97
+ context: {
98
+ ...snapshot.context,
99
+ selection: {
100
+ anchor: selectionEndPoint,
101
+ focus: selectionEndPoint,
102
+ },
103
+ },
104
+ })
105
+
106
+ const endPointEqualToOriginalStartPoint = isEqualSelectionPoints(
107
+ selectionEndPoint,
108
+ originalSelectionStartPoint,
109
+ )
110
+ const startPointEqualToOriginalEndPoint = isEqualSelectionPoints(
111
+ selectionStartPoint,
112
+ originalSelectionEndPoint,
113
+ )
114
+
115
+ if (endPointBeforeSelection && !endPointEqualToOriginalStartPoint) {
39
116
  return false
40
117
  }
41
118
 
42
- if (!isPointBeforeSelection(selectionEndPoint)(snapshot)) {
119
+ if (startPointAfterSelection && !startPointEqualToOriginalEndPoint) {
43
120
  return false
44
121
  }
45
122
 
46
- return true
123
+ if (
124
+ !originalStartPointBeforeStartPoint &&
125
+ originalStartPointAfterStartPoint &&
126
+ !originalEndPointBeforeEndPoint &&
127
+ originalEndPointAfterEndPoint
128
+ ) {
129
+ return !endPointEqualToOriginalStartPoint
130
+ }
131
+
132
+ if (
133
+ originalStartPointBeforeStartPoint &&
134
+ !originalStartPointAfterStartPoint &&
135
+ originalEndPointBeforeEndPoint &&
136
+ !originalEndPointAfterEndPoint
137
+ ) {
138
+ return !startPointEqualToOriginalEndPoint
139
+ }
140
+
141
+ if (
142
+ !startPointAfterSelection ||
143
+ !startPointBeforeSelection ||
144
+ !endPointAfterSelection ||
145
+ !endPointBeforeSelection
146
+ ) {
147
+ return true
148
+ }
149
+
150
+ return false
47
151
  }
48
152
  }
@@ -14,7 +14,9 @@ export function isPointAfterSelection(
14
14
  return false
15
15
  }
16
16
 
17
- const selection = reverseSelection(snapshot.context.selection)
17
+ const selection = snapshot.context.selection.backward
18
+ ? reverseSelection(snapshot.context.selection)
19
+ : snapshot.context.selection
18
20
 
19
21
  const pointBlockKey = isKeySegment(point.path[0])
20
22
  ? point.path[0]._key
@@ -14,7 +14,9 @@ export function isPointBeforeSelection(
14
14
  return false
15
15
  }
16
16
 
17
- const selection = reverseSelection(snapshot.context.selection)
17
+ const selection = snapshot.context.selection.backward
18
+ ? reverseSelection(snapshot.context.selection)
19
+ : snapshot.context.selection
18
20
 
19
21
  const pointBlockKey = isKeySegment(point.path[0])
20
22
  ? point.path[0]._key
@@ -0,0 +1,34 @@
1
+ import type {EditorSelector} from '../editor/editor-selector'
2
+ import * as utils from '../utils'
3
+ import {getSelectionEndBlock, getSelectionStartBlock} from './selectors'
4
+
5
+ /**
6
+ * @public
7
+ */
8
+ export const isSelectingEntireBlocks: EditorSelector<boolean> = (snapshot) => {
9
+ if (!snapshot.context.selection) {
10
+ return false
11
+ }
12
+
13
+ const startPoint = snapshot.context.selection.backward
14
+ ? snapshot.context.selection.focus
15
+ : snapshot.context.selection.anchor
16
+ const endPoint = snapshot.context.selection.backward
17
+ ? snapshot.context.selection.anchor
18
+ : snapshot.context.selection.focus
19
+
20
+ const startBlock = getSelectionStartBlock(snapshot)
21
+ const endBlock = getSelectionEndBlock(snapshot)
22
+
23
+ if (!startBlock || !endBlock) {
24
+ return false
25
+ }
26
+
27
+ const startBlockStartPoint = utils.getBlockStartPoint(startBlock)
28
+ const endBlockEndPoint = utils.getBlockEndPoint(endBlock)
29
+
30
+ return (
31
+ utils.isEqualSelectionPoints(startBlockStartPoint, startPoint) &&
32
+ utils.isEqualSelectionPoints(endBlockEndPoint, endPoint)
33
+ )
34
+ }