@portabletext/editor 2.0.0 → 2.1.1

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": "2.0.0",
3
+ "version": "2.1.1",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -78,26 +78,26 @@
78
78
  "slate": "0.117.2",
79
79
  "slate-dom": "^0.117.4",
80
80
  "slate-react": "0.117.4",
81
- "use-effect-event": "^2.0.3",
81
+ "use-effect-event": "^1.0.2",
82
82
  "xstate": "^5.20.1",
83
- "@portabletext/patches": "1.1.6",
83
+ "@portabletext/block-tools": "2.0.2",
84
84
  "@portabletext/keyboard-shortcuts": "1.1.1",
85
- "@portabletext/block-tools": "2.0.0"
85
+ "@portabletext/patches": "1.1.6"
86
86
  },
87
87
  "devDependencies": {
88
88
  "@portabletext/toolkit": "^2.0.17",
89
89
  "@sanity/diff-match-patch": "^3.2.0",
90
90
  "@sanity/pkg-utils": "^7.9.6",
91
- "@sanity/schema": "^4.0.1",
92
- "@sanity/types": "^4.0.1",
91
+ "@sanity/schema": "^4.2.0",
92
+ "@sanity/types": "^4.2.0",
93
93
  "@testing-library/react": "^16.3.0",
94
94
  "@types/debug": "^4.1.12",
95
95
  "@types/lodash": "^4.17.16",
96
96
  "@types/lodash.startcase": "^4.4.9",
97
- "@types/react": "^19.1.8",
98
- "@types/react-dom": "^19.1.6",
99
- "@typescript-eslint/eslint-plugin": "^8.35.0",
100
- "@typescript-eslint/parser": "^8.35.0",
97
+ "@types/react": "^19.1.9",
98
+ "@types/react-dom": "^19.1.7",
99
+ "@typescript-eslint/eslint-plugin": "^8.38.0",
100
+ "@typescript-eslint/parser": "^8.38.0",
101
101
  "@vitejs/plugin-react": "^4.6.0",
102
102
  "@vitest/browser": "^3.2.4",
103
103
  "@vitest/coverage-istanbul": "^3.2.4",
@@ -105,18 +105,18 @@
105
105
  "eslint": "8.57.1",
106
106
  "eslint-plugin-react-hooks": "0.0.0-experimental-dffacc7b-20250717",
107
107
  "jsdom": "^26.0.0",
108
- "react": "^19.1.0",
109
- "react-dom": "^19.1.0",
108
+ "react": "^19.1.1",
109
+ "react-dom": "^19.1.1",
110
110
  "rxjs": "^7.8.2",
111
- "typescript": "5.8.3",
112
- "vite": "^7.0.3",
111
+ "typescript": "5.9.2",
112
+ "vite": "^7.0.6",
113
113
  "vitest": "^3.2.4",
114
- "vitest-browser-react": "^1.0.0",
115
- "racejar": "1.2.10"
114
+ "vitest-browser-react": "^1.0.1",
115
+ "racejar": "1.2.11"
116
116
  },
117
117
  "peerDependencies": {
118
- "@sanity/schema": "^4.0.1",
119
- "@sanity/types": "^4.0.1",
118
+ "@sanity/schema": "^4.2.0",
119
+ "@sanity/types": "^4.2.0",
120
120
  "react": "^18.3 || ^19",
121
121
  "rxjs": "^7.8.2"
122
122
  },
@@ -2,6 +2,7 @@ import {isListBlock, isTextBlock} from '../internal-utils/parse-blocks'
2
2
  import {defaultKeyboardShortcuts} from '../keyboard-shortcuts/default-keyboard-shortcuts'
3
3
  import * as selectors from '../selectors'
4
4
  import {getBlockEndPoint} from '../utils'
5
+ import {isAtTheBeginningOfBlock} from '../utils/util.at-the-beginning-of-block'
5
6
  import {isEmptyTextBlock} from '../utils/util.is-empty-text-block'
6
7
  import {raise} from './behavior.types.action'
7
8
  import {defineBehavior} from './behavior.types.behavior'
@@ -11,23 +12,26 @@ const MAX_LIST_LEVEL = 10
11
12
  const clearListOnBackspace = defineBehavior({
12
13
  on: 'delete.backward',
13
14
  guard: ({snapshot}) => {
14
- const selectionCollapsed = selectors.isSelectionCollapsed(snapshot)
15
15
  const focusTextBlock = selectors.getFocusTextBlock(snapshot)
16
- const focusSpan = selectors.getFocusSpan(snapshot)
17
16
 
18
- if (!selectionCollapsed || !focusTextBlock || !focusSpan) {
17
+ if (!focusTextBlock) {
19
18
  return false
20
19
  }
21
20
 
22
- const atTheBeginningOfBLock =
23
- focusTextBlock.node.children[0]._key === focusSpan.node._key &&
24
- snapshot.context.selection?.focus.offset === 0
21
+ if (focusTextBlock.node.level !== 1) {
22
+ return false
23
+ }
25
24
 
26
- if (atTheBeginningOfBLock && focusTextBlock.node.level === 1) {
27
- return {focusTextBlock}
25
+ if (
26
+ !isAtTheBeginningOfBlock({
27
+ context: snapshot.context,
28
+ block: focusTextBlock.node,
29
+ })
30
+ ) {
31
+ return false
28
32
  }
29
33
 
30
- return false
34
+ return {focusTextBlock}
31
35
  },
32
36
  actions: [
33
37
  (_, {focusTextBlock}) => [
@@ -132,6 +136,15 @@ const mergeTextIntoListOnBackspace = defineBehavior({
132
136
  return false
133
137
  }
134
138
 
139
+ if (
140
+ !isAtTheBeginningOfBlock({
141
+ context: snapshot.context,
142
+ block: focusTextBlock.node,
143
+ })
144
+ ) {
145
+ return false
146
+ }
147
+
135
148
  if (!isListBlock(snapshot.context, previousBlock.node)) {
136
149
  return false
137
150
  }
@@ -33,6 +33,7 @@ describe('RangeDecorations', () => {
33
33
  anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
34
34
  focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
35
35
  },
36
+ payload: {id: 'a'},
36
37
  },
37
38
  ]
38
39
 
@@ -85,6 +86,7 @@ describe('RangeDecorations', () => {
85
86
  anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
86
87
  focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
87
88
  },
89
+ payload: {id: 'a'},
88
90
  },
89
91
  ]
90
92
  rerender(
@@ -111,6 +113,7 @@ describe('RangeDecorations', () => {
111
113
  anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
112
114
  focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 4},
113
115
  },
116
+ payload: {id: 'a'},
114
117
  },
115
118
  ]
116
119
  rerender(
@@ -138,6 +141,7 @@ describe('RangeDecorations', () => {
138
141
  anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
139
142
  focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
140
143
  },
144
+ payload: {id: 'a'},
141
145
  },
142
146
  ]
143
147
  rerender(
@@ -156,5 +160,61 @@ describe('RangeDecorations', () => {
156
160
  'updated-with-different',
157
161
  ])
158
162
  })
163
+
164
+ // Update the range decorations with a new payload
165
+ rangeDecorations = [
166
+ {
167
+ component: RangeDecorationTestComponent,
168
+ selection: {
169
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
170
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
171
+ },
172
+ payload: {id: 'b'},
173
+ },
174
+ ]
175
+ rerender(
176
+ <PortableTextEditorTester
177
+ keyGenerator={createTestKeyGenerator()}
178
+ onChange={onChange}
179
+ rangeDecorations={rangeDecorations}
180
+ ref={editorRef}
181
+ schemaType={schemaType}
182
+ value={value}
183
+ />,
184
+ )
185
+ await waitFor(() => {
186
+ expect([
187
+ rangeDecorationIteration,
188
+ 'updated-with-different-payload',
189
+ ]).toEqual([4, 'updated-with-different-payload'])
190
+ })
191
+
192
+ // Update the range decorations with a new payload again
193
+ rangeDecorations = [
194
+ {
195
+ component: RangeDecorationTestComponent,
196
+ selection: {
197
+ anchor: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 0},
198
+ focus: {path: [{_key: '123'}, 'children', {_key: '567'}], offset: 2},
199
+ },
200
+ payload: {id: 'c'},
201
+ },
202
+ ]
203
+ rerender(
204
+ <PortableTextEditorTester
205
+ keyGenerator={createTestKeyGenerator()}
206
+ onChange={onChange}
207
+ rangeDecorations={rangeDecorations}
208
+ ref={editorRef}
209
+ schemaType={schemaType}
210
+ value={value}
211
+ />,
212
+ )
213
+ await waitFor(() => {
214
+ expect([
215
+ rangeDecorationIteration,
216
+ 'updated-with-different-payload',
217
+ ]).toEqual([5, 'updated-with-different-payload'])
218
+ })
159
219
  })
160
220
  })
@@ -254,6 +254,7 @@ export const rangeDecorationsMachine = setup({
254
254
  (decoratedRange) => ({
255
255
  anchor: decoratedRange.rangeDecoration.selection?.anchor,
256
256
  focus: decoratedRange.rangeDecoration.selection?.focus,
257
+ payload: decoratedRange.rangeDecoration.payload,
257
258
  }),
258
259
  )
259
260
 
@@ -261,6 +262,7 @@ export const rangeDecorationsMachine = setup({
261
262
  (rangeDecoration) => ({
262
263
  anchor: rangeDecoration.selection?.anchor,
263
264
  focus: rangeDecoration.selection?.focus,
265
+ payload: rangeDecoration.payload,
264
266
  }),
265
267
  )
266
268
 
@@ -0,0 +1,32 @@
1
+ import type {PortableTextBlock} from '@sanity/types'
2
+ import type {EditorContext} from '../editor/editor-snapshot'
3
+ import {isTextBlock} from '../internal-utils/parse-blocks'
4
+ import {getChildKeyFromSelectionPoint} from '../selection/selection-point'
5
+ import {isSelectionCollapsed} from './util.is-selection-collapsed'
6
+
7
+ export function isAtTheBeginningOfBlock({
8
+ context,
9
+ block,
10
+ }: {
11
+ context: EditorContext
12
+ block: PortableTextBlock
13
+ }) {
14
+ if (!isTextBlock(context, block)) {
15
+ return false
16
+ }
17
+
18
+ if (!context.selection) {
19
+ return false
20
+ }
21
+
22
+ if (!isSelectionCollapsed(context.selection)) {
23
+ return false
24
+ }
25
+
26
+ const focusSpanKey = getChildKeyFromSelectionPoint(context.selection.focus)
27
+
28
+ return (
29
+ focusSpanKey === block.children[0]._key &&
30
+ context.selection.focus.offset === 0
31
+ )
32
+ }