@portabletext/editor 2.17.0 → 2.17.2

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,5 +1,5 @@
1
1
  import { Behavior, Editor, EditorEmittedEvent, EditorSchema } from "../_chunks-dts/behavior.types.action.cjs";
2
- import * as react12 from "react";
2
+ import * as react22 from "react";
3
3
  import React from "react";
4
4
  /**
5
5
  * @beta
@@ -181,7 +181,7 @@ type MarkdownPluginConfig = MarkdownBehaviorsConfig & {
181
181
  */
182
182
  declare function MarkdownPlugin(props: {
183
183
  config: MarkdownPluginConfig;
184
- }): react12.JSX.Element;
184
+ }): react22.JSX.Element;
185
185
  /**
186
186
  * @beta
187
187
  * Restrict the editor to one line. The plugin takes care of blocking
@@ -192,5 +192,5 @@ declare function MarkdownPlugin(props: {
192
192
  *
193
193
  * @deprecated Install the plugin from `@portabletext/plugin-one-line`
194
194
  */
195
- declare function OneLinePlugin(): react12.JSX.Element;
195
+ declare function OneLinePlugin(): react22.JSX.Element;
196
196
  export { BehaviorPlugin, DecoratorShortcutPlugin, EditorRefPlugin, EventListenerPlugin, MarkdownPlugin, MarkdownPluginConfig, OneLinePlugin };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "2.17.0",
3
+ "version": "2.17.2",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -76,7 +76,6 @@
76
76
  "@portabletext/to-html": "^3.0.0",
77
77
  "@xstate/react": "^6.0.0",
78
78
  "debug": "^4.4.3",
79
- "get-random-values-esm": "^1.0.2",
80
79
  "immer": "^10.2.0",
81
80
  "lodash": "^4.17.21",
82
81
  "lodash.startcase": "^4.4.0",
@@ -85,9 +84,9 @@
85
84
  "slate-dom": "^0.118.1",
86
85
  "slate-react": "0.118.2",
87
86
  "xstate": "^5.23.0",
88
- "@portabletext/block-tools": "^3.5.13",
89
- "@portabletext/patches": "^1.1.8",
87
+ "@portabletext/block-tools": "^3.5.14",
90
88
  "@portabletext/keyboard-shortcuts": "^1.1.1",
89
+ "@portabletext/patches": "^1.1.8",
91
90
  "@portabletext/schema": "^1.2.0"
92
91
  },
93
92
  "devDependencies": {
@@ -102,9 +101,9 @@
102
101
  "@types/react": "^19.2.0",
103
102
  "@types/react-dom": "^19.2.0",
104
103
  "@vitejs/plugin-react": "^5.0.4",
105
- "@vitest/browser": "^4.0.4",
106
- "@vitest/browser-playwright": "^4.0.4",
107
- "@vitest/coverage-istanbul": "^4.0.4",
104
+ "@vitest/browser": "^4.0.5",
105
+ "@vitest/browser-playwright": "^4.0.5",
106
+ "@vitest/coverage-istanbul": "^4.0.5",
108
107
  "babel-plugin-react-compiler": "1.0.0",
109
108
  "eslint": "^9.38.0",
110
109
  "eslint-formatter-gha": "^1.6.0",
@@ -116,14 +115,14 @@
116
115
  "typescript": "5.9.3",
117
116
  "typescript-eslint": "^8.46.1",
118
117
  "vite": "^7.1.10",
119
- "vitest": "^4.0.4",
118
+ "vitest": "^4.0.5",
120
119
  "vitest-browser-react": "^2.0.2",
121
- "@portabletext/sanity-bridge": "1.1.16",
120
+ "@portabletext/sanity-bridge": "1.1.17",
122
121
  "@portabletext/test": "^0.0.0",
123
122
  "racejar": "1.3.2"
124
123
  },
125
124
  "peerDependencies": {
126
- "@portabletext/sanity-bridge": "^1.1.16",
125
+ "@portabletext/sanity-bridge": "^1.1.17",
127
126
  "@sanity/schema": "^4.12.0",
128
127
  "@sanity/types": "^4.12.0",
129
128
  "react": "^18.3 || ^19",
@@ -17,19 +17,14 @@ export const abstractDeleteBehaviors = [
17
17
  defineBehavior({
18
18
  on: 'delete.backward',
19
19
  guard: ({snapshot}) => {
20
- if (!snapshot.context.selection) {
21
- return false
22
- }
23
-
24
- return {selection: snapshot.context.selection}
20
+ return snapshot.context.selection
25
21
  },
26
22
  actions: [
27
- ({event}, {selection}) => [
23
+ ({event}) => [
28
24
  raise({
29
25
  type: 'delete',
30
26
  direction: 'backward',
31
27
  unit: event.unit,
32
- at: selection,
33
28
  }),
34
29
  ],
35
30
  ],
@@ -88,19 +83,14 @@ export const abstractDeleteBehaviors = [
88
83
  defineBehavior({
89
84
  on: 'delete.forward',
90
85
  guard: ({snapshot}) => {
91
- if (!snapshot.context.selection) {
92
- return false
93
- }
94
-
95
- return {selection: snapshot.context.selection}
86
+ return snapshot.context.selection
96
87
  },
97
88
  actions: [
98
- ({event}, {selection}) => [
89
+ ({event}) => [
99
90
  raise({
100
91
  type: 'delete',
101
92
  direction: 'forward',
102
93
  unit: event.unit,
103
- at: selection,
104
94
  }),
105
95
  ],
106
96
  ],
@@ -112,18 +102,24 @@ export const abstractDeleteBehaviors = [
112
102
  return false
113
103
  }
114
104
 
105
+ const at = event.at ?? snapshot.context.selection
106
+
107
+ if (!at) {
108
+ return false
109
+ }
110
+
115
111
  const nextBlock = getNextBlock({
116
112
  ...snapshot,
117
113
  context: {
118
114
  ...snapshot.context,
119
- selection: event.at,
115
+ selection: at,
120
116
  },
121
117
  })
122
118
  const focusTextBlock = getFocusTextBlock({
123
119
  ...snapshot,
124
120
  context: {
125
121
  ...snapshot.context,
126
- selection: event.at,
122
+ selection: at,
127
123
  },
128
124
  })
129
125
 
@@ -1,6 +1,6 @@
1
+ import {isSelectionExpanded} from '../selectors'
1
2
  import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
2
3
  import {getLastBlock} from '../selectors/selector.get-last-block'
3
- import {isSelectionCollapsed} from '../utils'
4
4
  import {getBlockEndPoint} from '../utils/util.get-block-end-point'
5
5
  import {getBlockStartPoint} from '../utils/util.get-block-start-point'
6
6
  import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
@@ -473,20 +473,51 @@ export const abstractInsertBehaviors = [
473
473
  ],
474
474
  ],
475
475
  }),
476
+
477
+ /**
478
+ * If there's an expanded selection, then we delete the selection before we
479
+ * insert the text.
480
+ */
476
481
  defineBehavior({
477
482
  on: 'insert.text',
478
483
  guard: ({snapshot}) => {
479
- const selection = snapshot.context.selection
484
+ return isSelectionExpanded(snapshot)
485
+ },
486
+ actions: [({event}) => [raise({type: 'delete'}), raise(event)]],
487
+ }),
488
+
489
+ /**
490
+ * If there's no selection, then we select the end of the editor before we
491
+ * we insert the text.
492
+ */
493
+ defineBehavior({
494
+ on: 'insert.text',
495
+ guard: ({snapshot}) => {
496
+ if (snapshot.context.selection) {
497
+ return false
498
+ }
499
+
500
+ const lastBlok = getLastBlock(snapshot)
480
501
 
481
- if (!selection || isSelectionCollapsed(selection)) {
502
+ if (!lastBlok) {
482
503
  return false
483
504
  }
484
505
 
485
- return {selection}
506
+ const endPoint = getBlockEndPoint({
507
+ context: snapshot.context,
508
+ block: lastBlok,
509
+ })
510
+ return {endPoint}
486
511
  },
487
512
  actions: [
488
- ({event}, {selection}) => [
489
- raise({type: 'delete', at: selection}),
513
+ ({event}, {endPoint}) => [
514
+ raise({
515
+ type: 'select',
516
+ at: {
517
+ anchor: endPoint,
518
+ focus: endPoint,
519
+ },
520
+ }),
490
521
  raise(event),
491
522
  ],
492
523
  ],
@@ -1,10 +1,11 @@
1
1
  import {isTextBlock} from '@portabletext/schema'
2
+ import {isSelectionExpanded} from '../selectors'
2
3
  import {getFocusBlockObject} from '../selectors/selector.get-focus-block-object'
3
4
  import {getFocusInlineObject} from '../selectors/selector.get-focus-inline-object'
4
5
  import {getFocusTextBlock} from '../selectors/selector.get-focus-text-block'
5
- import {getSelectedValue} from '../selectors/selector.get-selected-value'
6
6
  import {getSelectionEndBlock} from '../selectors/selector.get-selection-end-block'
7
7
  import {getSelectionStartBlock} from '../selectors/selector.get-selection-start-block'
8
+ import {isEqualSelectionPoints} from '../utils'
8
9
  import {parseBlock} from '../utils/parse-blocks'
9
10
  import {getBlockEndPoint} from '../utils/util.get-block-end-point'
10
11
  import {getBlockStartPoint} from '../utils/util.get-block-start-point'
@@ -47,115 +48,49 @@ export const abstractSplitBehaviors = [
47
48
  return false
48
49
  }
49
50
 
50
- const selectionStartBlock = getSelectionStartBlock(snapshot)
51
- const selectionEndBlock = getSelectionEndBlock(snapshot)
52
-
53
- if (!selectionStartBlock || !selectionEndBlock) {
54
- return false
55
- }
56
-
57
- if (
58
- !isTextBlock(snapshot.context, selectionStartBlock.node) &&
59
- isTextBlock(snapshot.context, selectionEndBlock.node)
60
- ) {
61
- return {selection}
62
- }
63
-
64
- return false
65
- },
66
- actions: [(_, {selection}) => [raise({type: 'delete', at: selection})]],
67
- }),
68
-
69
- defineBehavior({
70
- on: 'split',
71
- guard: ({snapshot}) => {
72
- const selection = snapshot.context.selection
51
+ const startPoint = getSelectionStartPoint(selection)
52
+ const endPoint = getSelectionEndPoint(selection)
73
53
 
74
- if (!selection || isSelectionCollapsed(selection)) {
54
+ if (!startPoint || !endPoint) {
75
55
  return false
76
56
  }
77
57
 
78
- const selectionStartBlock = getSelectionStartBlock(snapshot)
79
- const selectionEndBlock = getSelectionEndBlock(snapshot)
80
-
81
- if (!selectionStartBlock || !selectionEndBlock) {
82
- return false
83
- }
58
+ const startBlock = getSelectionStartBlock(snapshot)
59
+ const endBlock = getSelectionEndBlock(snapshot)
84
60
 
85
- if (selectionStartBlock.node._key === selectionEndBlock.node._key) {
61
+ if (!startBlock || !endBlock) {
86
62
  return false
87
63
  }
88
64
 
89
- const startPoint = getSelectionStartPoint(selection)
90
- const startBlockEndPoint = getBlockEndPoint({
65
+ const startBlockStartPoint = getBlockStartPoint({
91
66
  context: snapshot.context,
92
- block: selectionStartBlock,
67
+ block: startBlock,
93
68
  })
94
- const endPoint = getSelectionEndPoint(selection)
95
- const endBlockStartPoint = getBlockStartPoint({
69
+ const endBlockEndPoint = getBlockEndPoint({
96
70
  context: snapshot.context,
97
- block: selectionEndBlock,
71
+ block: endBlock,
98
72
  })
99
73
 
100
- const selectedValue = getSelectedValue(snapshot)
101
-
102
- const blocksInBetween = selectedValue.filter(
103
- (block) =>
104
- block._key !== selectionStartBlock.node._key &&
105
- block._key !== selectionEndBlock.node._key,
106
- )
107
-
108
- return {
109
- startPoint,
110
- startBlockEndPoint,
111
- endPoint,
112
- endBlockStartPoint,
113
- blocksInBetween,
74
+ if (
75
+ isTextBlock(snapshot.context, startBlock.node) &&
76
+ isTextBlock(snapshot.context, endBlock.node) &&
77
+ !isEqualSelectionPoints(startPoint, startBlockStartPoint) &&
78
+ !isEqualSelectionPoints(endPoint, endBlockEndPoint)
79
+ ) {
80
+ return true
114
81
  }
82
+
83
+ return false
115
84
  },
116
- actions: [
117
- (
118
- _,
119
- {
120
- startPoint,
121
- startBlockEndPoint,
122
- endPoint,
123
- endBlockStartPoint,
124
- blocksInBetween,
125
- },
126
- ) => [
127
- raise({
128
- type: 'delete',
129
- at: {anchor: startPoint, focus: startBlockEndPoint},
130
- }),
131
- ...blocksInBetween.map((block) =>
132
- raise({type: 'delete.block', at: [{_key: block._key}]}),
133
- ),
134
- raise({
135
- type: 'delete',
136
- at: {anchor: endBlockStartPoint, focus: endPoint},
137
- }),
138
- ],
139
- ],
85
+ actions: [() => [raise({type: 'delete'}), raise({type: 'split'})]],
140
86
  }),
141
87
 
142
88
  defineBehavior({
143
89
  on: 'split',
144
90
  guard: ({snapshot}) => {
145
- const selection = snapshot.context.selection
146
-
147
- if (!selection || isSelectionCollapsed(selection)) {
148
- return false
149
- }
150
-
151
- return {selection}
91
+ return isSelectionExpanded(snapshot)
152
92
  },
153
- actions: [
154
- (_, {selection}) => [
155
- raise({type: 'delete', at: selection}),
156
- raise({type: 'split'}),
157
- ],
158
- ],
93
+ actions: [() => [raise({type: 'delete'})]],
159
94
  }),
160
95
 
161
96
  defineBehavior({
@@ -204,11 +204,17 @@ const mergeTextIntoListOnBackspace = defineBehavior({
204
204
  const deletingListFromStart = defineBehavior({
205
205
  on: 'delete',
206
206
  guard: ({snapshot, event}) => {
207
+ const at = event.at ?? snapshot.context.selection
208
+
209
+ if (!at) {
210
+ return false
211
+ }
212
+
207
213
  const blocksToDelete = getSelectedBlocks({
208
214
  ...snapshot,
209
215
  context: {
210
216
  ...snapshot.context,
211
- selection: event.at,
217
+ selection: at,
212
218
  },
213
219
  })
214
220
 
@@ -233,14 +239,14 @@ const deletingListFromStart = defineBehavior({
233
239
  ...snapshot,
234
240
  context: {
235
241
  ...snapshot.context,
236
- selection: event.at,
242
+ selection: at,
237
243
  },
238
244
  })
239
245
  const deleteEndPoint = getSelectionEndPoint({
240
246
  ...snapshot,
241
247
  context: {
242
248
  ...snapshot.context,
243
- selection: event.at,
249
+ selection: at,
244
250
  },
245
251
  })
246
252
 
@@ -140,7 +140,7 @@ export type SyntheticBehaviorEvent =
140
140
  }
141
141
  | {
142
142
  type: StrictExtract<SyntheticBehaviorEventType, 'delete'>
143
- at: NonNullable<EditorSelection>
143
+ at?: NonNullable<EditorSelection>
144
144
  /**
145
145
  * Defaults to forward deletion.
146
146
  */
@@ -18,38 +18,37 @@ export function createWithEventListeners(editorActor: EditorActor) {
18
18
  return
19
19
  }
20
20
 
21
- const at = options?.at ?? editor.selection
22
-
23
- if (!at) {
24
- console.error('Unexpected call to .delete(...) without `at` option')
25
- return
26
- }
27
-
28
- const range = Editor.range(editor, at)
29
-
30
- const selection = slateRangeToSelection({
31
- schema: editorActor.getSnapshot().context.schema,
32
- editor,
33
- range,
34
- })
35
-
36
- if (!selection) {
37
- console.error(
38
- 'Unexpected call to .delete(...) with invalid `at` option',
39
- )
40
- return
21
+ const range = options?.at ? Editor.range(editor, options.at) : undefined
22
+ const selection = range
23
+ ? slateRangeToSelection({
24
+ schema: editorActor.getSnapshot().context.schema,
25
+ editor,
26
+ range,
27
+ })
28
+ : undefined
29
+
30
+ if (selection) {
31
+ editorActor.send({
32
+ type: 'behavior event',
33
+ behaviorEvent: {
34
+ type: 'delete',
35
+ at: selection,
36
+ direction: options?.reverse ? 'backward' : 'forward',
37
+ unit: options?.unit,
38
+ },
39
+ editor,
40
+ })
41
+ } else {
42
+ editorActor.send({
43
+ type: 'behavior event',
44
+ behaviorEvent: {
45
+ type: 'delete',
46
+ direction: options?.reverse ? 'backward' : 'forward',
47
+ unit: options?.unit,
48
+ },
49
+ editor,
50
+ })
41
51
  }
42
-
43
- editorActor.send({
44
- type: 'behavior event',
45
- behaviorEvent: {
46
- type: 'delete',
47
- at: selection,
48
- direction: options?.reverse ? 'backward' : 'forward',
49
- unit: options?.unit,
50
- },
51
- editor,
52
- })
53
52
  }
54
53
 
55
54
  editor.deleteBackward = (unit) => {
@@ -1,14 +1,8 @@
1
1
  import {isTextBlock} from '@portabletext/schema'
2
- import {
3
- deleteText,
4
- Editor,
5
- Element,
6
- Range,
7
- setSelection,
8
- Transforms,
9
- } from 'slate'
2
+ import {deleteText, Editor, Element, Range, Transforms} from 'slate'
10
3
  import {DOMEditor} from 'slate-dom'
11
4
  import {createPlaceholderBlock} from '../internal-utils/create-placeholder-block'
5
+ import {slateRangeToSelection} from '../internal-utils/slate-utils'
12
6
  import {toSlateRange} from '../internal-utils/to-slate-range'
13
7
  import type {PortableTextSlateEditor} from '../types/editor'
14
8
  import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
@@ -17,48 +11,54 @@ import type {BehaviorOperationImplementation} from './behavior.operations'
17
11
  export const deleteOperationImplementation: BehaviorOperationImplementation<
18
12
  'delete'
19
13
  > = ({context, operation}) => {
20
- const anchorBlockKey = getBlockKeyFromSelectionPoint(operation.at.anchor)
21
- const focusBlockKey = getBlockKeyFromSelectionPoint(operation.at.focus)
22
-
23
- const startBlockKey = operation.at.backward ? focusBlockKey : anchorBlockKey
24
- const endBlockKey = operation.at.backward ? anchorBlockKey : focusBlockKey
25
- const endOffset = operation.at.backward
26
- ? operation.at.focus.offset
27
- : operation.at.anchor.offset
28
-
29
- if (!startBlockKey) {
30
- throw new Error('Failed to get start block key')
31
- }
32
-
33
- if (!endBlockKey) {
34
- throw new Error('Failed to get end block key')
35
- }
36
-
37
- const startBlockIndex = operation.editor.blockIndexMap.get(startBlockKey)
38
-
39
- if (startBlockIndex === undefined) {
40
- throw new Error('Failed to get start block index')
41
- }
42
-
43
- const startBlock = operation.editor.value.at(startBlockIndex)
44
-
45
- if (!startBlock) {
46
- throw new Error('Failed to get start block')
47
- }
48
-
49
- const endBlockIndex = operation.editor.blockIndexMap.get(endBlockKey)
50
-
51
- if (endBlockIndex === undefined) {
52
- throw new Error('Failed to get end block index')
53
- }
54
-
55
- const endBlock = operation.editor.value.at(endBlockIndex)
56
-
57
- if (!endBlock) {
58
- throw new Error('Failed to get end block')
59
- }
14
+ const at = operation.at
15
+ ? toSlateRange({
16
+ context: {
17
+ schema: context.schema,
18
+ value: operation.editor.value,
19
+ selection: operation.at,
20
+ },
21
+ blockIndexMap: operation.editor.blockIndexMap,
22
+ })
23
+ : undefined
24
+
25
+ const selection = operation.editor.selection
26
+ ? slateRangeToSelection({
27
+ schema: context.schema,
28
+ editor: operation.editor,
29
+ range: operation.editor.selection,
30
+ })
31
+ : undefined
32
+
33
+ const reverse = operation.direction === 'backward'
34
+ const anchorPoint = operation.at?.anchor ?? selection?.anchor
35
+ const focusPoint = operation.at?.focus ?? selection?.focus
36
+ const startPoint = reverse ? focusPoint : anchorPoint
37
+ const endPoint = reverse ? anchorPoint : focusPoint
38
+ const startBlockKey = startPoint
39
+ ? getBlockKeyFromSelectionPoint(startPoint)
40
+ : undefined
41
+ const endBlockKey = endPoint
42
+ ? getBlockKeyFromSelectionPoint(endPoint)
43
+ : undefined
44
+ const startBlockIndex = startBlockKey
45
+ ? operation.editor.blockIndexMap.get(startBlockKey)
46
+ : undefined
47
+ const endBlockIndex = endBlockKey
48
+ ? operation.editor.blockIndexMap.get(endBlockKey)
49
+ : undefined
50
+ const startBlock = startBlockIndex
51
+ ? operation.editor.value.at(startBlockIndex)
52
+ : undefined
53
+ const endBlock = endBlockIndex
54
+ ? operation.editor.value.at(endBlockIndex)
55
+ : undefined
60
56
 
61
57
  if (operation.unit === 'block') {
58
+ if (startBlockIndex === undefined || endBlockIndex === undefined) {
59
+ throw new Error('Failed to get start or end block index')
60
+ }
61
+
62
62
  Transforms.removeNodes(operation.editor, {
63
63
  at: {
64
64
  anchor: {path: [startBlockIndex], offset: 0},
@@ -74,22 +74,13 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
74
74
  return
75
75
  }
76
76
 
77
- const range = toSlateRange({
78
- context: {
79
- schema: context.schema,
80
- value: operation.editor.value,
81
- selection: operation.at,
82
- },
83
- blockIndexMap: operation.editor.blockIndexMap,
84
- })
85
-
86
- if (!range) {
87
- throw new Error(
88
- `Failed to get Slate Range for selection ${JSON.stringify(operation.at)}`,
89
- )
90
- }
91
-
92
77
  if (operation.direction === 'backward' && operation.unit === 'line') {
78
+ const range = at ?? operation.editor.selection ?? undefined
79
+
80
+ if (!range) {
81
+ throw new Error('Unable to delete line without a selection')
82
+ }
83
+
93
84
  const parentBlockEntry = Editor.above(operation.editor, {
94
85
  match: (n) => Element.isElement(n) && Editor.isBlock(operation.editor, n),
95
86
  at: range,
@@ -115,23 +106,28 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
115
106
  }
116
107
  }
117
108
 
118
- const hanging = isTextBlock(context, endBlock) && endOffset === 0
119
-
120
- deleteText(operation.editor, {
121
- at: range,
122
- reverse: operation.direction === 'backward',
123
- unit: operation.unit,
124
- hanging,
125
- })
126
-
127
- if (
128
- operation.editor.selection &&
129
- isTextBlock(context, startBlock) &&
130
- isTextBlock(context, endBlock)
131
- ) {
132
- setSelection(operation.editor, {
133
- anchor: operation.editor.selection.focus,
134
- focus: operation.editor.selection.focus,
109
+ const hanging = reverse
110
+ ? endPoint
111
+ ? isTextBlock(context, endBlock)
112
+ ? endPoint.offset === 0
113
+ : true
114
+ : false
115
+ : startPoint
116
+ ? isTextBlock(context, startBlock)
117
+ ? startPoint.offset === 0
118
+ : true
119
+ : false
120
+
121
+ if (at) {
122
+ deleteText(operation.editor, {
123
+ at,
124
+ hanging,
125
+ reverse,
126
+ })
127
+ } else {
128
+ deleteText(operation.editor, {
129
+ hanging,
130
+ reverse,
135
131
  })
136
132
  }
137
133
  }