@portabletext/editor 1.55.0 → 1.55.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@portabletext/editor",
3
- "version": "1.55.0",
3
+ "version": "1.55.2",
4
4
  "description": "Portable Text Editor made in React",
5
5
  "keywords": [
6
6
  "sanity",
@@ -86,7 +86,7 @@
86
86
  "devDependencies": {
87
87
  "@portabletext/toolkit": "^2.0.17",
88
88
  "@sanity/diff-match-patch": "^3.2.0",
89
- "@sanity/pkg-utils": "^7.8.4",
89
+ "@sanity/pkg-utils": "^7.8.5",
90
90
  "@sanity/schema": "^3.93.0",
91
91
  "@sanity/types": "^3.93.0",
92
92
  "@testing-library/react": "^16.3.0",
@@ -98,8 +98,8 @@
98
98
  "@typescript-eslint/eslint-plugin": "^8.34.0",
99
99
  "@typescript-eslint/parser": "^8.34.0",
100
100
  "@vitejs/plugin-react": "^4.5.2",
101
- "@vitest/browser": "^3.2.3",
102
- "@vitest/coverage-istanbul": "^3.2.3",
101
+ "@vitest/browser": "^3.2.4",
102
+ "@vitest/coverage-istanbul": "^3.2.4",
103
103
  "babel-plugin-react-compiler": "19.1.0-rc.2",
104
104
  "eslint": "8.57.1",
105
105
  "eslint-plugin-react-hooks": "0.0.0-experimental-12bc60f5-20250613",
@@ -109,9 +109,9 @@
109
109
  "rxjs": "^7.8.2",
110
110
  "typescript": "5.8.3",
111
111
  "vite": "^6.2.5",
112
- "vitest": "^3.2.3",
112
+ "vitest": "^3.2.4",
113
113
  "vitest-browser-react": "^0.3.0",
114
- "racejar": "1.2.7"
114
+ "racejar": "1.2.8"
115
115
  },
116
116
  "peerDependencies": {
117
117
  "@sanity/schema": "^3.93.0",
@@ -46,15 +46,20 @@ export function createSlateEditor(config: SlateEditorConfig): SlateEditor {
46
46
  config.editorActor.getSnapshot().context,
47
47
  )
48
48
  instance.value = [placeholderBlock]
49
+ instance.blockIndexMap = new Map<string, number>()
50
+ instance.listIndexMap = new Map<string, number>()
49
51
 
50
- const {blockIndexMap, listIndexMap} = buildIndexMaps(
51
- config.editorActor.getSnapshot().context,
52
- instance.value,
52
+ buildIndexMaps(
53
+ {
54
+ schema: config.editorActor.getSnapshot().context.schema,
55
+ value: instance.value,
56
+ },
57
+ {
58
+ blockIndexMap: instance.blockIndexMap,
59
+ listIndexMap: instance.listIndexMap,
60
+ },
53
61
  )
54
62
 
55
- instance.blockIndexMap = blockIndexMap
56
- instance.listIndexMap = listIndexMap
57
-
58
63
  const initialValue = toSlateValue(instance.value, {
59
64
  schemaTypes: config.editorActor.getSnapshot().context.schema,
60
65
  })
@@ -21,9 +21,16 @@ export function pluginUpdateValue(
21
21
  operation,
22
22
  )
23
23
 
24
- const {blockIndexMap, listIndexMap} = buildIndexMaps(context, editor.value)
25
- editor.blockIndexMap = blockIndexMap
26
- editor.listIndexMap = listIndexMap
24
+ buildIndexMaps(
25
+ {
26
+ schema: context.schema,
27
+ value: editor.value,
28
+ },
29
+ {
30
+ blockIndexMap: editor.blockIndexMap,
31
+ listIndexMap: editor.listIndexMap,
32
+ },
33
+ )
27
34
 
28
35
  apply(operation)
29
36
  }
@@ -44,168 +44,210 @@ const schema = compileSchemaDefinition(
44
44
  )
45
45
 
46
46
  describe(buildIndexMaps.name, () => {
47
+ const blockIndexMap = new Map<string, number>()
48
+ const listIndexMap = new Map<string, number>()
49
+
47
50
  test('empty', () => {
48
- expect(buildIndexMaps({schema}, [])).toEqual({
49
- blockIndexMap: new Map(),
50
- listIndexMap: new Map(),
51
- })
51
+ buildIndexMaps({schema, value: []}, {blockIndexMap, listIndexMap})
52
+ expect(blockIndexMap).toEqual(new Map())
53
+ expect(listIndexMap).toEqual(new Map())
52
54
  })
53
55
 
54
56
  test('single list item', () => {
55
- expect(
56
- buildIndexMaps({schema}, [
57
- textBlock('k0', {listItem: 'number', level: 1}),
58
- ]),
59
- ).toEqual({
60
- blockIndexMap: new Map([['k0', 0]]),
61
- listIndexMap: new Map([['k0', 1]]),
62
- })
57
+ buildIndexMaps(
58
+ {schema, value: [textBlock('k0', {listItem: 'number', level: 1})]},
59
+ {blockIndexMap, listIndexMap},
60
+ )
61
+ expect(blockIndexMap).toEqual(new Map([['k0', 0]]))
62
+ expect(listIndexMap).toEqual(new Map([['k0', 1]]))
63
63
  })
64
64
 
65
65
  test('single indented list item', () => {
66
- expect(
67
- buildIndexMaps({schema}, [
68
- textBlock('k0', {listItem: 'number', level: 2}),
69
- ]),
70
- ).toEqual({
71
- blockIndexMap: new Map([['k0', 0]]),
72
- listIndexMap: new Map([['k0', 1]]),
73
- })
66
+ buildIndexMaps(
67
+ {schema, value: [textBlock('k0', {listItem: 'number', level: 2})]},
68
+ {blockIndexMap, listIndexMap},
69
+ )
70
+ expect(blockIndexMap).toEqual(new Map([['k0', 0]]))
71
+ expect(listIndexMap).toEqual(new Map([['k0', 1]]))
74
72
  })
75
73
 
76
74
  test('two lists broken up by a paragraph', () => {
77
- expect(
78
- buildIndexMaps({schema}, [
79
- textBlock('k0', {listItem: 'number', level: 1}),
80
- textBlock('k1', {listItem: 'number', level: 1}),
81
- textBlock('k2', {}),
82
- textBlock('k3', {listItem: 'number', level: 1}),
83
- textBlock('k4', {listItem: 'number', level: 1}),
84
- ]),
85
- ).toEqual({
86
- blockIndexMap: new Map([
75
+ buildIndexMaps(
76
+ {
77
+ schema,
78
+ value: [
79
+ textBlock('k0', {listItem: 'number', level: 1}),
80
+ textBlock('k1', {listItem: 'number', level: 1}),
81
+ textBlock('k2', {}),
82
+ textBlock('k3', {listItem: 'number', level: 1}),
83
+ textBlock('k4', {listItem: 'number', level: 1}),
84
+ ],
85
+ },
86
+ {blockIndexMap, listIndexMap},
87
+ )
88
+ expect(blockIndexMap).toEqual(
89
+ new Map([
87
90
  ['k0', 0],
88
91
  ['k1', 1],
89
92
  ['k2', 2],
90
93
  ['k3', 3],
91
94
  ['k4', 4],
92
95
  ]),
93
- listIndexMap: new Map([
96
+ )
97
+ expect(listIndexMap).toEqual(
98
+ new Map([
94
99
  ['k0', 1],
95
100
  ['k1', 2],
96
101
  ['k3', 1],
97
102
  ['k4', 2],
98
103
  ]),
99
- })
104
+ )
100
105
  })
101
106
 
102
107
  test('two lists broken up by an image', () => {
103
- expect(
104
- buildIndexMaps({schema}, [
105
- textBlock('k0', {listItem: 'number', level: 1}),
106
- textBlock('k1', {listItem: 'number', level: 1}),
107
- blockObject('k2', 'image'),
108
- textBlock('k3', {listItem: 'number', level: 1}),
109
- textBlock('k4', {listItem: 'number', level: 1}),
110
- ]),
111
- ).toEqual({
112
- blockIndexMap: new Map([
108
+ buildIndexMaps(
109
+ {
110
+ schema,
111
+ value: [
112
+ textBlock('k0', {listItem: 'number', level: 1}),
113
+ textBlock('k1', {listItem: 'number', level: 1}),
114
+ blockObject('k2', 'image'),
115
+ textBlock('k3', {listItem: 'number', level: 1}),
116
+ textBlock('k4', {listItem: 'number', level: 1}),
117
+ ],
118
+ },
119
+ {blockIndexMap, listIndexMap},
120
+ )
121
+ expect(blockIndexMap).toEqual(
122
+ new Map([
113
123
  ['k0', 0],
114
124
  ['k1', 1],
115
125
  ['k2', 2],
116
126
  ['k3', 3],
117
127
  ['k4', 4],
118
128
  ]),
119
- listIndexMap: new Map([
129
+ )
130
+ expect(listIndexMap).toEqual(
131
+ new Map([
120
132
  ['k0', 1],
121
133
  ['k1', 2],
122
134
  ['k3', 1],
123
135
  ['k4', 2],
124
136
  ]),
125
- })
137
+ )
126
138
  })
127
139
 
128
140
  test('numbered lists broken up by a bulleted list', () => {
129
141
  expect(
130
- buildIndexMaps({schema}, [
131
- textBlock('k0', {listItem: 'number', level: 1}),
132
- textBlock('k1', {listItem: 'bullet', level: 1}),
133
- textBlock('k2', {listItem: 'number', level: 1}),
134
- ]),
135
- ).toEqual({
136
- blockIndexMap: new Map([
142
+ buildIndexMaps(
143
+ {
144
+ schema,
145
+ value: [
146
+ textBlock('k0', {listItem: 'number', level: 1}),
147
+ textBlock('k1', {listItem: 'bullet', level: 1}),
148
+ textBlock('k2', {listItem: 'number', level: 1}),
149
+ ],
150
+ },
151
+ {blockIndexMap, listIndexMap},
152
+ ),
153
+ )
154
+ expect(blockIndexMap).toEqual(
155
+ new Map([
137
156
  ['k0', 0],
138
157
  ['k1', 1],
139
158
  ['k2', 2],
140
159
  ]),
141
- listIndexMap: new Map([
160
+ )
161
+ expect(listIndexMap).toEqual(
162
+ new Map([
142
163
  ['k0', 1],
143
164
  ['k1', 1],
144
165
  ['k2', 1],
145
166
  ]),
146
- })
167
+ )
147
168
  })
148
169
 
149
170
  test('simple indented list', () => {
150
- expect(
151
- buildIndexMaps({schema}, [
152
- textBlock('k0', {listItem: 'number', level: 1}),
153
- textBlock('k1', {listItem: 'number', level: 2}),
154
- textBlock('k2', {listItem: 'number', level: 2}),
155
- textBlock('k3', {listItem: 'number', level: 1}),
156
- ]),
157
- ).toEqual({
158
- blockIndexMap: new Map([
171
+ buildIndexMaps(
172
+ {
173
+ schema,
174
+ value: [
175
+ textBlock('k0', {listItem: 'number', level: 1}),
176
+ textBlock('k1', {listItem: 'number', level: 2}),
177
+ textBlock('k2', {listItem: 'number', level: 2}),
178
+ textBlock('k3', {listItem: 'number', level: 1}),
179
+ ],
180
+ },
181
+ {blockIndexMap, listIndexMap},
182
+ )
183
+ expect(blockIndexMap).toEqual(
184
+ new Map([
159
185
  ['k0', 0],
160
186
  ['k1', 1],
161
187
  ['k2', 2],
162
188
  ['k3', 3],
163
189
  ]),
164
- listIndexMap: new Map([
190
+ )
191
+ expect(listIndexMap).toEqual(
192
+ new Map([
165
193
  ['k0', 1],
166
194
  ['k1', 1],
167
195
  ['k2', 2],
168
196
  ['k3', 2],
169
197
  ]),
170
- })
198
+ )
171
199
  })
172
200
 
173
201
  test('reverse indented list', () => {
174
202
  expect(
175
- buildIndexMaps({schema}, [
176
- textBlock('k0', {listItem: 'number', level: 2}),
177
- textBlock('k1', {listItem: 'number', level: 1}),
178
- textBlock('k2', {listItem: 'number', level: 2}),
179
- ]),
180
- ).toEqual({
181
- blockIndexMap: new Map([
203
+ buildIndexMaps(
204
+ {
205
+ schema,
206
+ value: [
207
+ textBlock('k0', {listItem: 'number', level: 2}),
208
+ textBlock('k1', {listItem: 'number', level: 1}),
209
+ textBlock('k2', {listItem: 'number', level: 2}),
210
+ ],
211
+ },
212
+ {blockIndexMap, listIndexMap},
213
+ ),
214
+ )
215
+ expect(blockIndexMap).toEqual(
216
+ new Map([
182
217
  ['k0', 0],
183
218
  ['k1', 1],
184
219
  ['k2', 2],
185
220
  ]),
186
- listIndexMap: new Map([
221
+ )
222
+ expect(listIndexMap).toEqual(
223
+ new Map([
187
224
  ['k0', 1],
188
225
  ['k1', 1],
189
226
  ['k2', 1],
190
227
  ]),
191
- })
228
+ )
192
229
  })
193
230
 
194
231
  test('complex list', () => {
195
- expect(
196
- buildIndexMaps({schema}, [
197
- textBlock('k0', {listItem: 'number', level: 1}),
198
- textBlock('k1', {listItem: 'number', level: 3}),
199
- textBlock('k2', {listItem: 'number', level: 2}),
200
- textBlock('k3', {listItem: 'number', level: 3}),
201
- textBlock('k4', {listItem: 'number', level: 1}),
202
- textBlock('k5', {listItem: 'number', level: 3}),
203
- textBlock('k6', {listItem: 'number', level: 4}),
204
- textBlock('k7', {listItem: 'number', level: 3}),
205
- textBlock('k8', {listItem: 'number', level: 1}),
206
- ]),
207
- ).toEqual({
208
- blockIndexMap: new Map([
232
+ buildIndexMaps(
233
+ {
234
+ schema,
235
+ value: [
236
+ textBlock('k0', {listItem: 'number', level: 1}),
237
+ textBlock('k1', {listItem: 'number', level: 3}),
238
+ textBlock('k2', {listItem: 'number', level: 2}),
239
+ textBlock('k3', {listItem: 'number', level: 3}),
240
+ textBlock('k4', {listItem: 'number', level: 1}),
241
+ textBlock('k5', {listItem: 'number', level: 3}),
242
+ textBlock('k6', {listItem: 'number', level: 4}),
243
+ textBlock('k7', {listItem: 'number', level: 3}),
244
+ textBlock('k8', {listItem: 'number', level: 1}),
245
+ ],
246
+ },
247
+ {blockIndexMap, listIndexMap},
248
+ )
249
+ expect(blockIndexMap).toEqual(
250
+ new Map([
209
251
  ['k0', 0],
210
252
  ['k1', 1],
211
253
  ['k2', 2],
@@ -216,7 +258,9 @@ describe(buildIndexMaps.name, () => {
216
258
  ['k7', 7],
217
259
  ['k8', 8],
218
260
  ]),
219
- listIndexMap: new Map([
261
+ )
262
+ expect(listIndexMap).toEqual(
263
+ new Map([
220
264
  ['k0', 1],
221
265
  ['k1', 1],
222
266
  ['k2', 1],
@@ -227,6 +271,6 @@ describe(buildIndexMaps.name, () => {
227
271
  ['k7', 2],
228
272
  ['k8', 3],
229
273
  ]),
230
- })
274
+ )
231
275
  })
232
276
  })
@@ -1,17 +1,25 @@
1
- import type {PortableTextBlock} from '@sanity/types'
2
1
  import type {EditorContext} from '../editor/editor-snapshot'
3
2
  import {isTextBlock} from './parse-blocks'
4
3
 
4
+ const levelIndexMap = new Map<number, number>()
5
+
6
+ /**
7
+ * Mutates the maps in place.
8
+ */
5
9
  export function buildIndexMaps(
6
- context: Pick<EditorContext, 'schema'>,
7
- value: Array<PortableTextBlock>,
8
- ): {
9
- blockIndexMap: Map<string, number>
10
- listIndexMap: Map<string, number>
11
- } {
12
- const blockIndexMap = new Map<string, number>()
13
- const listIndexMap = new Map<string, number>()
14
- const levelIndexMap = new Map<number, number>()
10
+ context: Pick<EditorContext, 'schema' | 'value'>,
11
+ {
12
+ blockIndexMap,
13
+ listIndexMap,
14
+ }: {
15
+ blockIndexMap: Map<string, number>
16
+ listIndexMap: Map<string, number>
17
+ },
18
+ ): void {
19
+ blockIndexMap.clear()
20
+ listIndexMap.clear()
21
+ levelIndexMap.clear()
22
+
15
23
  let previousListItem:
16
24
  | {
17
25
  listItem: string
@@ -19,8 +27,8 @@ export function buildIndexMaps(
19
27
  }
20
28
  | undefined
21
29
 
22
- for (let blockIndex = 0; blockIndex < value.length; blockIndex++) {
23
- const block = value.at(blockIndex)
30
+ for (let blockIndex = 0; blockIndex < context.value.length; blockIndex++) {
31
+ const block = context.value.at(blockIndex)
24
32
 
25
33
  if (block === undefined) {
26
34
  continue
@@ -92,6 +100,4 @@ export function buildIndexMaps(
92
100
  listIndexMap.set(block._key, levelCounter + 1)
93
101
  }
94
102
  }
95
-
96
- return {blockIndexMap, listIndexMap}
97
103
  }
@@ -1,39 +1,62 @@
1
- import {isKeySegment, type Path} from '@sanity/types'
2
- import {isEqual} from 'lodash'
3
- import {Editor, Element, type Descendant, type Path as SlatePath} from 'slate'
1
+ import {Element, type Editor, type Path} from 'slate'
2
+ import type {EditorSelectionPoint} from '..'
3
+ import {
4
+ getBlockKeyFromSelectionPoint,
5
+ getChildKeyFromSelectionPoint,
6
+ } from '../selection/selection-point'
4
7
 
5
- export function toSlatePath(path: Path, editor: Editor): SlatePath {
6
- if (!editor) {
8
+ export function toSlatePath(
9
+ path: EditorSelectionPoint['path'],
10
+ editor: Editor,
11
+ ): Path {
12
+ const blockKey = getBlockKeyFromSelectionPoint({
13
+ path,
14
+ offset: 0,
15
+ })
16
+
17
+ if (!blockKey) {
18
+ return []
19
+ }
20
+
21
+ const blockIndex = editor.blockIndexMap.get(blockKey)
22
+
23
+ if (blockIndex === undefined) {
7
24
  return []
8
25
  }
9
- const [block, blockPath] = Array.from(
10
- Editor.nodes(editor, {
11
- at: [],
12
- match: (n) =>
13
- isKeySegment(path[0]) && (n as Descendant)._key === path[0]._key,
14
- }),
15
- )[0] || [undefined, undefined]
26
+
27
+ const block = editor.children.at(blockIndex)
16
28
 
17
29
  if (!block || !Element.isElement(block)) {
18
30
  return []
19
31
  }
20
32
 
21
33
  if (editor.isVoid(block)) {
22
- return [blockPath[0], 0]
34
+ return [blockIndex, 0]
35
+ }
36
+
37
+ const childKey = getChildKeyFromSelectionPoint({
38
+ path,
39
+ offset: 0,
40
+ })
41
+
42
+ if (!childKey) {
43
+ return [blockIndex, 0]
23
44
  }
24
45
 
25
- const childPath = [path[2]]
26
- const childIndex = block.children.findIndex((child) =>
27
- isEqual([{_key: child._key}], childPath),
28
- )
46
+ let childPath: Array<number> = []
47
+ let childIndex = -1
29
48
 
30
- if (childIndex >= 0 && block.children[childIndex]) {
31
- const child = block.children[childIndex]
32
- if (Element.isElement(child) && editor.isVoid(child)) {
33
- return blockPath.concat(childIndex).concat(0)
49
+ for (const child of block.children) {
50
+ childIndex++
51
+ if (child._key === childKey) {
52
+ if (Element.isElement(child) && editor.isVoid(child)) {
53
+ childPath = [childIndex, 0]
54
+ } else {
55
+ childPath = [childIndex]
56
+ }
57
+ break
34
58
  }
35
- return blockPath.concat(childIndex)
36
59
  }
37
60
 
38
- return [blockPath[0], 0]
61
+ return [blockIndex].concat(childPath)
39
62
  }
@@ -15,6 +15,7 @@ export function toSlateRange(
15
15
  if (!selection || !editor) {
16
16
  return null
17
17
  }
18
+
18
19
  const anchor = {
19
20
  path: toSlatePath(selection.anchor.path, editor),
20
21
  offset: selection.anchor.offset,
@@ -23,10 +24,13 @@ export function toSlateRange(
23
24
  path: toSlatePath(selection.focus.path, editor),
24
25
  offset: selection.focus.offset,
25
26
  }
27
+
26
28
  if (focus.path.length === 0 || anchor.path.length === 0) {
27
29
  return null
28
30
  }
31
+
29
32
  const range = anchor && focus ? {anchor, focus} : null
33
+
30
34
  return range
31
35
  }
32
36