@portabletext/editor 3.2.2 → 3.2.3
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/lib/index.js +104 -49
- package/lib/index.js.map +1 -1
- package/package.json +10 -10
- package/src/editor/plugins/createWithPortableTextMarkModel.ts +6 -2
- package/src/operations/behavior.operation.delete.ts +131 -62
- package/src/test/gherkin-parameter-types.ts +5 -0
- package/src/test/vitest/step-definitions.tsx +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@portabletext/editor",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
4
4
|
"description": "Portable Text Editor made in React",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"sanity",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"slate-dom": "^0.119.0",
|
|
74
74
|
"slate-react": "0.120.0",
|
|
75
75
|
"xstate": "^5.24.0",
|
|
76
|
-
"@portabletext/block-tools": "^4.1.
|
|
76
|
+
"@portabletext/block-tools": "^4.1.5",
|
|
77
77
|
"@portabletext/keyboard-shortcuts": "^2.1.0",
|
|
78
78
|
"@portabletext/patches": "^2.0.0",
|
|
79
79
|
"@portabletext/schema": "^2.0.0"
|
|
@@ -81,8 +81,8 @@
|
|
|
81
81
|
"devDependencies": {
|
|
82
82
|
"@sanity/diff-match-patch": "^3.2.0",
|
|
83
83
|
"@sanity/pkg-utils": "^10.0.0",
|
|
84
|
-
"@sanity/schema": "^4.20.
|
|
85
|
-
"@sanity/types": "^4.20.
|
|
84
|
+
"@sanity/schema": "^4.20.1",
|
|
85
|
+
"@sanity/types": "^4.20.1",
|
|
86
86
|
"@types/debug": "^4.1.12",
|
|
87
87
|
"@types/lodash": "^4.17.20",
|
|
88
88
|
"@types/lodash.startcase": "^4.4.9",
|
|
@@ -94,7 +94,7 @@
|
|
|
94
94
|
"@vitest/browser-playwright": "^4.0.14",
|
|
95
95
|
"@vitest/coverage-istanbul": "^4.0.14",
|
|
96
96
|
"babel-plugin-react-compiler": "1.0.0",
|
|
97
|
-
"eslint": "^9.
|
|
97
|
+
"eslint": "^9.39.1",
|
|
98
98
|
"eslint-formatter-gha": "^1.6.0",
|
|
99
99
|
"eslint-plugin-react-hooks": "7.0.1",
|
|
100
100
|
"jsdom": "^27.0.0",
|
|
@@ -102,18 +102,18 @@
|
|
|
102
102
|
"react-dom": "^19.2.1",
|
|
103
103
|
"rxjs": "^7.8.2",
|
|
104
104
|
"typescript": "5.9.3",
|
|
105
|
-
"typescript-eslint": "^8.
|
|
105
|
+
"typescript-eslint": "^8.48.0",
|
|
106
106
|
"vite": "^7.1.12",
|
|
107
107
|
"vitest": "^4.0.14",
|
|
108
108
|
"vitest-browser-react": "^2.0.2",
|
|
109
|
-
"@portabletext/sanity-bridge": "1.2.
|
|
109
|
+
"@portabletext/sanity-bridge": "1.2.8",
|
|
110
110
|
"@portabletext/test": "^1.0.0",
|
|
111
111
|
"racejar": "2.0.0"
|
|
112
112
|
},
|
|
113
113
|
"peerDependencies": {
|
|
114
|
-
"@portabletext/sanity-bridge": "^1.2.
|
|
115
|
-
"@sanity/schema": "^4.20.
|
|
116
|
-
"@sanity/types": "^4.20.
|
|
114
|
+
"@portabletext/sanity-bridge": "^1.2.8",
|
|
115
|
+
"@sanity/schema": "^4.20.1",
|
|
116
|
+
"@sanity/types": "^4.20.1",
|
|
117
117
|
"react": "^18.3 || ^19",
|
|
118
118
|
"rxjs": "^7.8.2"
|
|
119
119
|
},
|
|
@@ -356,12 +356,16 @@ export function createWithPortableTextMarkModel(
|
|
|
356
356
|
op.properties.focus.offset === 0 &&
|
|
357
357
|
newFocusSpan.text.length === op.newProperties.focus.offset
|
|
358
358
|
|
|
359
|
-
//
|
|
360
|
-
//
|
|
359
|
+
// In the case of a collapsed selection moving to another collapsed
|
|
360
|
+
// selection, we only want to clear the decorator state if the
|
|
361
|
+
// caret is visually moving to a different span.
|
|
361
362
|
if (!movedToNextSpan && !movedToPreviousSpan) {
|
|
362
363
|
editor.decoratorState = {}
|
|
363
364
|
}
|
|
364
365
|
}
|
|
366
|
+
} else {
|
|
367
|
+
// In any other case, we want to clear the decorator state.
|
|
368
|
+
editor.decoratorState = {}
|
|
365
369
|
}
|
|
366
370
|
}
|
|
367
371
|
|
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
import {isSpan, isTextBlock} from '@portabletext/schema'
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
deleteText,
|
|
4
|
+
Editor,
|
|
5
|
+
Element,
|
|
6
|
+
Path,
|
|
7
|
+
Point,
|
|
8
|
+
Range,
|
|
9
|
+
Transforms,
|
|
10
|
+
type NodeEntry,
|
|
11
|
+
} from 'slate'
|
|
3
12
|
import {DOMEditor} from 'slate-dom'
|
|
4
|
-
import {slateRangeToSelection} from '../internal-utils/slate-utils'
|
|
5
13
|
import {toSlateRange} from '../internal-utils/to-slate-range'
|
|
6
14
|
import {VOID_CHILD_KEY} from '../internal-utils/values'
|
|
7
15
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
8
|
-
import {getBlockKeyFromSelectionPoint} from '../utils/util.selection-point'
|
|
9
16
|
import type {BehaviorOperationImplementation} from './behavior.operations'
|
|
10
17
|
|
|
11
18
|
export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
@@ -20,41 +27,18 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
20
27
|
},
|
|
21
28
|
blockIndexMap: operation.editor.blockIndexMap,
|
|
22
29
|
})
|
|
23
|
-
:
|
|
30
|
+
: operation.editor.selection
|
|
24
31
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
editor: operation.editor,
|
|
29
|
-
range: operation.editor.selection,
|
|
30
|
-
})
|
|
31
|
-
: undefined
|
|
32
|
+
if (!at) {
|
|
33
|
+
throw new Error('Unable to delete without a selection')
|
|
34
|
+
}
|
|
32
35
|
|
|
33
|
-
const
|
|
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
|
|
36
|
+
const [start, end] = Range.edges(at)
|
|
56
37
|
|
|
57
38
|
if (operation.unit === 'block') {
|
|
39
|
+
const startBlockIndex = start.path.at(0)
|
|
40
|
+
const endBlockIndex = end.path.at(0)
|
|
41
|
+
|
|
58
42
|
if (startBlockIndex === undefined || endBlockIndex === undefined) {
|
|
59
43
|
throw new Error('Failed to get start or end block index')
|
|
60
44
|
}
|
|
@@ -71,14 +55,8 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
71
55
|
}
|
|
72
56
|
|
|
73
57
|
if (operation.unit === 'child') {
|
|
74
|
-
const range = at ?? operation.editor.selection ?? undefined
|
|
75
|
-
|
|
76
|
-
if (!range) {
|
|
77
|
-
throw new Error('Unable to delete children without a selection')
|
|
78
|
-
}
|
|
79
|
-
|
|
80
58
|
Transforms.removeNodes(operation.editor, {
|
|
81
|
-
at
|
|
59
|
+
at,
|
|
82
60
|
match: (node) =>
|
|
83
61
|
(isSpan(context, node) && node._key !== VOID_CHILD_KEY) ||
|
|
84
62
|
('__inline' in node && node.__inline === true),
|
|
@@ -88,15 +66,9 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
88
66
|
}
|
|
89
67
|
|
|
90
68
|
if (operation.direction === 'backward' && operation.unit === 'line') {
|
|
91
|
-
const range = at ?? operation.editor.selection ?? undefined
|
|
92
|
-
|
|
93
|
-
if (!range) {
|
|
94
|
-
throw new Error('Unable to delete line without a selection')
|
|
95
|
-
}
|
|
96
|
-
|
|
97
69
|
const parentBlockEntry = Editor.above(operation.editor, {
|
|
98
70
|
match: (n) => Element.isElement(n) && Editor.isBlock(operation.editor, n),
|
|
99
|
-
at
|
|
71
|
+
at,
|
|
100
72
|
})
|
|
101
73
|
|
|
102
74
|
if (parentBlockEntry) {
|
|
@@ -104,7 +76,7 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
104
76
|
const parentElementRange = Editor.range(
|
|
105
77
|
operation.editor,
|
|
106
78
|
parentBlockPath,
|
|
107
|
-
|
|
79
|
+
at.anchor,
|
|
108
80
|
)
|
|
109
81
|
|
|
110
82
|
const currentLineRange = findCurrentLineRange(
|
|
@@ -120,15 +92,9 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
120
92
|
}
|
|
121
93
|
|
|
122
94
|
if (operation.unit === 'word') {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (!range) {
|
|
126
|
-
throw new Error('Unable to delete word without a selection')
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (Range.isCollapsed(range)) {
|
|
95
|
+
if (Range.isCollapsed(at)) {
|
|
130
96
|
deleteText(operation.editor, {
|
|
131
|
-
at
|
|
97
|
+
at,
|
|
132
98
|
unit: 'word',
|
|
133
99
|
reverse: operation.direction === 'backward',
|
|
134
100
|
})
|
|
@@ -137,19 +103,122 @@ export const deleteOperationImplementation: BehaviorOperationImplementation<
|
|
|
137
103
|
}
|
|
138
104
|
}
|
|
139
105
|
|
|
106
|
+
const startBlock = Editor.above(operation.editor, {
|
|
107
|
+
match: (n) => Element.isElement(n) && Editor.isBlock(operation.editor, n),
|
|
108
|
+
at: start,
|
|
109
|
+
voids: false,
|
|
110
|
+
})
|
|
111
|
+
const endBlock = Editor.above(operation.editor, {
|
|
112
|
+
match: (n) => Element.isElement(n) && Editor.isBlock(operation.editor, n),
|
|
113
|
+
at: end,
|
|
114
|
+
voids: false,
|
|
115
|
+
})
|
|
116
|
+
const isAcrossBlocks =
|
|
117
|
+
startBlock && endBlock && !Path.equals(startBlock[1], endBlock[1])
|
|
118
|
+
const startNonEditable =
|
|
119
|
+
Editor.void(operation.editor, {at: start, mode: 'highest'}) ??
|
|
120
|
+
Editor.elementReadOnly(operation.editor, {at: start, mode: 'highest'})
|
|
121
|
+
const endNonEditable =
|
|
122
|
+
Editor.void(operation.editor, {at: end, mode: 'highest'}) ??
|
|
123
|
+
Editor.elementReadOnly(operation.editor, {at: end, mode: 'highest'})
|
|
124
|
+
|
|
125
|
+
const matches: NodeEntry[] = []
|
|
126
|
+
let lastPath: Path | undefined
|
|
127
|
+
|
|
128
|
+
for (const entry of Editor.nodes(operation.editor, {at, voids: false})) {
|
|
129
|
+
const [node, path] = entry
|
|
130
|
+
|
|
131
|
+
if (lastPath && Path.compare(path, lastPath) === 0) {
|
|
132
|
+
continue
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
(Element.isElement(node) &&
|
|
137
|
+
(Editor.isVoid(operation.editor, node) ||
|
|
138
|
+
Editor.isElementReadOnly(operation.editor, node))) ||
|
|
139
|
+
(!Path.isCommon(path, start.path) && !Path.isCommon(path, end.path))
|
|
140
|
+
) {
|
|
141
|
+
matches.push(entry)
|
|
142
|
+
lastPath = path
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const pathRefs = Array.from(matches, ([, path]) =>
|
|
147
|
+
Editor.pathRef(operation.editor, path),
|
|
148
|
+
)
|
|
149
|
+
const startRef = Editor.pointRef(operation.editor, start)
|
|
150
|
+
const endRef = Editor.pointRef(operation.editor, end)
|
|
151
|
+
|
|
152
|
+
const endToEndSelection =
|
|
153
|
+
startBlock &&
|
|
154
|
+
endBlock &&
|
|
155
|
+
Point.equals(start, Editor.start(operation.editor, startBlock[1])) &&
|
|
156
|
+
Point.equals(end, Editor.end(operation.editor, endBlock[1]))
|
|
157
|
+
|
|
158
|
+
if (
|
|
159
|
+
endToEndSelection &&
|
|
160
|
+
isAcrossBlocks &&
|
|
161
|
+
!startNonEditable &&
|
|
162
|
+
!endNonEditable
|
|
163
|
+
) {
|
|
164
|
+
if (!startNonEditable) {
|
|
165
|
+
const point = startRef.current!
|
|
166
|
+
const [node] = Editor.leaf(operation.editor, point)
|
|
167
|
+
|
|
168
|
+
if (node.text.length > 0) {
|
|
169
|
+
operation.editor.apply({
|
|
170
|
+
type: 'remove_text',
|
|
171
|
+
path: point.path,
|
|
172
|
+
offset: 0,
|
|
173
|
+
text: node.text,
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
for (const pathRef of pathRefs.reverse()) {
|
|
179
|
+
const path = pathRef.unref()
|
|
180
|
+
|
|
181
|
+
if (path) {
|
|
182
|
+
Transforms.removeNodes(operation.editor, {at: path, voids: false})
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!endNonEditable) {
|
|
187
|
+
const point = endRef.current!
|
|
188
|
+
const [node] = Editor.leaf(operation.editor, point)
|
|
189
|
+
const {path} = point
|
|
190
|
+
const offset = 0
|
|
191
|
+
const text = node.text.slice(offset, end.offset)
|
|
192
|
+
|
|
193
|
+
if (text.length > 0) {
|
|
194
|
+
operation.editor.apply({type: 'remove_text', path, offset, text})
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (endRef.current && startRef.current) {
|
|
199
|
+
Transforms.removeNodes(operation.editor, {
|
|
200
|
+
at: endRef.current,
|
|
201
|
+
voids: false,
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const reverse = operation.direction === 'backward'
|
|
140
209
|
const hanging = reverse
|
|
141
|
-
?
|
|
210
|
+
? end
|
|
142
211
|
? isTextBlock(context, endBlock)
|
|
143
|
-
?
|
|
212
|
+
? end.offset === 0
|
|
144
213
|
: true
|
|
145
214
|
: false
|
|
146
|
-
:
|
|
215
|
+
: start
|
|
147
216
|
? isTextBlock(context, startBlock)
|
|
148
|
-
?
|
|
217
|
+
? start.offset === 0
|
|
149
218
|
: true
|
|
150
219
|
: false
|
|
151
220
|
|
|
152
|
-
if (at) {
|
|
221
|
+
if (operation.at) {
|
|
153
222
|
deleteText(operation.editor, {
|
|
154
223
|
at,
|
|
155
224
|
hanging,
|
|
@@ -30,6 +30,10 @@ const parameterType = {
|
|
|
30
30
|
name: 'decorator',
|
|
31
31
|
matcher: /"(code|em|strong)"/,
|
|
32
32
|
}),
|
|
33
|
+
direction: createParameterType<'forwards' | 'backwards'>({
|
|
34
|
+
name: 'direction',
|
|
35
|
+
matcher: /"(forwards|backwards)"/,
|
|
36
|
+
}),
|
|
33
37
|
index: createParameterType({
|
|
34
38
|
name: 'index',
|
|
35
39
|
matcher: /"(\d)"/,
|
|
@@ -93,6 +97,7 @@ export const parameterTypes = [
|
|
|
93
97
|
parameterType.blockObject,
|
|
94
98
|
parameterType.button,
|
|
95
99
|
parameterType.decorator,
|
|
100
|
+
parameterType.direction,
|
|
96
101
|
parameterType.index,
|
|
97
102
|
parameterType.inlineObject,
|
|
98
103
|
parameterType.key,
|
|
@@ -227,6 +227,9 @@ export const stepDefinitions = [
|
|
|
227
227
|
const previousSelection = context.editor.getSnapshot().context.selection
|
|
228
228
|
await userEvent.keyboard(button)
|
|
229
229
|
|
|
230
|
+
// Small delay to allow the browser to process the event
|
|
231
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
232
|
+
|
|
230
233
|
await vi.waitFor(() => {
|
|
231
234
|
const currentSelection = context.editor.getSnapshot().context.selection
|
|
232
235
|
|
|
@@ -242,6 +245,9 @@ export const stepDefinitions = [
|
|
|
242
245
|
const previousSelection = context.editorB.getSnapshot().context.selection
|
|
243
246
|
await userEvent.keyboard(button)
|
|
244
247
|
|
|
248
|
+
// Small delay to allow the browser to process the event
|
|
249
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
250
|
+
|
|
245
251
|
await vi.waitFor(() => {
|
|
246
252
|
const currentSelection = context.editorB.getSnapshot().context.selection
|
|
247
253
|
|
|
@@ -258,6 +264,8 @@ export const stepDefinitions = [
|
|
|
258
264
|
const previousSelection = context.editor.getSnapshot().context.selection
|
|
259
265
|
await userEvent.keyboard(button)
|
|
260
266
|
|
|
267
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
268
|
+
|
|
261
269
|
await vi.waitFor(() => {
|
|
262
270
|
const currentSelection =
|
|
263
271
|
context.editor.getSnapshot().context.selection
|
|
@@ -277,6 +285,9 @@ export const stepDefinitions = [
|
|
|
277
285
|
context.editorB.getSnapshot().context.selection
|
|
278
286
|
await userEvent.keyboard(button)
|
|
279
287
|
|
|
288
|
+
// Small delay to allow the browser to process the event
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
290
|
+
|
|
280
291
|
await vi.waitFor(() => {
|
|
281
292
|
const currentSelection =
|
|
282
293
|
context.editorB.getSnapshot().context.selection
|
|
@@ -303,6 +314,9 @@ export const stepDefinitions = [
|
|
|
303
314
|
const previousSelection = context.editor.getSnapshot().context.selection
|
|
304
315
|
await userEvent.keyboard(shortcuts[shortcut])
|
|
305
316
|
|
|
317
|
+
// Small delay to allow the browser to process the event
|
|
318
|
+
await new Promise((resolve) => setTimeout(resolve, 50))
|
|
319
|
+
|
|
306
320
|
await vi.waitFor(() => {
|
|
307
321
|
const currentSelection = context.editor.getSnapshot().context.selection
|
|
308
322
|
|
|
@@ -471,6 +485,28 @@ export const stepDefinitions = [
|
|
|
471
485
|
})
|
|
472
486
|
})
|
|
473
487
|
}),
|
|
488
|
+
When(
|
|
489
|
+
'{string} is selected {direction}',
|
|
490
|
+
async (
|
|
491
|
+
context: Context,
|
|
492
|
+
text: string,
|
|
493
|
+
direction: Parameter['direction'],
|
|
494
|
+
) => {
|
|
495
|
+
await vi.waitFor(() => {
|
|
496
|
+
const selection = getTextSelection(
|
|
497
|
+
context.editor.getSnapshot().context,
|
|
498
|
+
text,
|
|
499
|
+
)
|
|
500
|
+
expect(selection).not.toBeNull()
|
|
501
|
+
|
|
502
|
+
context.editor.send({
|
|
503
|
+
type: 'select',
|
|
504
|
+
at:
|
|
505
|
+
direction === 'forwards' ? selection : reverseSelection(selection),
|
|
506
|
+
})
|
|
507
|
+
})
|
|
508
|
+
},
|
|
509
|
+
),
|
|
474
510
|
When(
|
|
475
511
|
'{string} is selected backwards',
|
|
476
512
|
async (context: Context, text: string) => {
|