@portabletext/editor 1.40.3 → 1.41.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.
- package/lib/_chunks-cjs/editor-provider.cjs +72 -34
- package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
- package/lib/_chunks-cjs/util.is-selection-collapsed.cjs +10 -0
- package/lib/_chunks-cjs/util.is-selection-collapsed.cjs.map +1 -0
- package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
- package/lib/_chunks-es/editor-provider.js +73 -35
- package/lib/_chunks-es/editor-provider.js.map +1 -1
- package/lib/_chunks-es/util.is-selection-collapsed.js +11 -0
- package/lib/_chunks-es/util.is-selection-collapsed.js.map +1 -0
- package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
- package/lib/index.cjs +307 -144
- package/lib/index.cjs.map +1 -1
- package/lib/index.js +309 -145
- package/lib/index.js.map +1 -1
- package/lib/utils/index.cjs +7 -5
- package/lib/utils/index.cjs.map +1 -1
- package/lib/utils/index.d.cts +23 -2
- package/lib/utils/index.d.ts +23 -2
- package/lib/utils/index.js +6 -3
- package/lib/utils/index.js.map +1 -1
- package/package.json +4 -4
- package/src/behavior-actions/behavior.action.insert-blocks.ts +5 -1
- package/src/converters/converter.text-plain.ts +24 -11
- package/src/editor/Editable.tsx +336 -223
- package/src/editor/components/drop-indicator.tsx +4 -1
- package/src/internal-utils/drag-selection.test.ts +74 -1
- package/src/internal-utils/drag-selection.ts +20 -4
- package/src/internal-utils/dragging-on-drag-origin.ts +22 -0
- package/src/internal-utils/event-position.ts +69 -10
- package/src/internal-utils/slate-utils.ts +74 -6
- package/src/utils/index.ts +2 -0
- package/src/utils/util.get-selection-end-point.ts +20 -0
- package/src/utils/util.get-selection-start-point.ts +20 -0
- package/src/utils/util.is-keyed-segment.ts +2 -2
|
@@ -41,10 +41,11 @@ describe(getDragSelection.name, () => {
|
|
|
41
41
|
{
|
|
42
42
|
_key: keyGenerator(),
|
|
43
43
|
_type: 'span',
|
|
44
|
-
text: '
|
|
44
|
+
text: 'baz',
|
|
45
45
|
},
|
|
46
46
|
],
|
|
47
47
|
}
|
|
48
|
+
const bazPath = [{_key: baz._key}, 'children', {_key: baz.children[0]._key}]
|
|
48
49
|
const image = {
|
|
49
50
|
_key: keyGenerator(),
|
|
50
51
|
_type: 'image',
|
|
@@ -262,6 +263,78 @@ describe(getDragSelection.name, () => {
|
|
|
262
263
|
})
|
|
263
264
|
})
|
|
264
265
|
|
|
266
|
+
test('dragging two text blocks with the top drag handle', () => {
|
|
267
|
+
expect(
|
|
268
|
+
getDragSelection({
|
|
269
|
+
eventSelection: {
|
|
270
|
+
anchor: {
|
|
271
|
+
path: fooPath,
|
|
272
|
+
offset: 0,
|
|
273
|
+
},
|
|
274
|
+
focus: {
|
|
275
|
+
path: fooPath,
|
|
276
|
+
offset: 0,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
snapshot: snapshot({
|
|
280
|
+
anchor: {
|
|
281
|
+
path: fooPath,
|
|
282
|
+
offset: 1,
|
|
283
|
+
},
|
|
284
|
+
focus: {
|
|
285
|
+
path: bazPath,
|
|
286
|
+
offset: 3,
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
}),
|
|
290
|
+
).toEqual({
|
|
291
|
+
anchor: {
|
|
292
|
+
path: fooPath,
|
|
293
|
+
offset: 0,
|
|
294
|
+
},
|
|
295
|
+
focus: {
|
|
296
|
+
path: bazPath,
|
|
297
|
+
offset: 3,
|
|
298
|
+
},
|
|
299
|
+
})
|
|
300
|
+
})
|
|
301
|
+
|
|
302
|
+
test('dragging two text blocks with the bottom drag handle', () => {
|
|
303
|
+
expect(
|
|
304
|
+
getDragSelection({
|
|
305
|
+
eventSelection: {
|
|
306
|
+
anchor: {
|
|
307
|
+
path: bazPath,
|
|
308
|
+
offset: 0,
|
|
309
|
+
},
|
|
310
|
+
focus: {
|
|
311
|
+
path: bazPath,
|
|
312
|
+
offset: 0,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
snapshot: snapshot({
|
|
316
|
+
anchor: {
|
|
317
|
+
path: fooPath,
|
|
318
|
+
offset: 1,
|
|
319
|
+
},
|
|
320
|
+
focus: {
|
|
321
|
+
path: bazPath,
|
|
322
|
+
offset: 3,
|
|
323
|
+
},
|
|
324
|
+
}),
|
|
325
|
+
}),
|
|
326
|
+
).toEqual({
|
|
327
|
+
anchor: {
|
|
328
|
+
path: fooPath,
|
|
329
|
+
offset: 0,
|
|
330
|
+
},
|
|
331
|
+
focus: {
|
|
332
|
+
path: bazPath,
|
|
333
|
+
offset: 3,
|
|
334
|
+
},
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
265
338
|
test('dragging a block object with an expanded selected', () => {
|
|
266
339
|
expect(
|
|
267
340
|
getDragSelection({
|
|
@@ -48,16 +48,32 @@ export function getDragSelection({
|
|
|
48
48
|
if (
|
|
49
49
|
snapshot.context.selection &&
|
|
50
50
|
selectors.isSelectionExpanded(snapshot) &&
|
|
51
|
-
selectors.isOverlappingSelection(eventSelection)(snapshot) &&
|
|
52
51
|
selectedBlocks.length > 1
|
|
53
52
|
) {
|
|
54
53
|
const selectionStartBlock = selectors.getSelectionStartBlock(snapshot)
|
|
55
54
|
const selectionEndBlock = selectors.getSelectionEndBlock(snapshot)
|
|
56
55
|
|
|
57
|
-
if (selectionStartBlock
|
|
56
|
+
if (!selectionStartBlock || !selectionEndBlock) {
|
|
57
|
+
return dragSelection
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const selectionStartPoint = utils.getBlockStartPoint(selectionStartBlock)
|
|
61
|
+
const selectionEndPoint = utils.getBlockEndPoint(selectionEndBlock)
|
|
62
|
+
|
|
63
|
+
const eventSelectionInsideBlocks = selectors.isOverlappingSelection(
|
|
64
|
+
eventSelection,
|
|
65
|
+
)({
|
|
66
|
+
...snapshot,
|
|
67
|
+
context: {
|
|
68
|
+
...snapshot.context,
|
|
69
|
+
selection: {anchor: selectionStartPoint, focus: selectionEndPoint},
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
if (eventSelectionInsideBlocks) {
|
|
58
74
|
dragSelection = {
|
|
59
|
-
anchor:
|
|
60
|
-
focus:
|
|
75
|
+
anchor: selectionStartPoint,
|
|
76
|
+
focus: selectionEndPoint,
|
|
61
77
|
}
|
|
62
78
|
}
|
|
63
79
|
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {EditorSnapshot} from '..'
|
|
2
|
+
import * as selectors from '../selectors'
|
|
3
|
+
import type {EventPosition} from './event-position'
|
|
4
|
+
|
|
5
|
+
export function draggingOnDragOrigin({
|
|
6
|
+
snapshot,
|
|
7
|
+
position,
|
|
8
|
+
}: {
|
|
9
|
+
snapshot: EditorSnapshot
|
|
10
|
+
position: EventPosition
|
|
11
|
+
}) {
|
|
12
|
+
const dragOrigin = snapshot.beta.internalDrag?.origin
|
|
13
|
+
return dragOrigin
|
|
14
|
+
? selectors.isOverlappingSelection(position.selection)({
|
|
15
|
+
...snapshot,
|
|
16
|
+
context: {
|
|
17
|
+
...snapshot.context,
|
|
18
|
+
selection: dragOrigin.selection,
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
: false
|
|
22
|
+
}
|
|
@@ -4,6 +4,7 @@ import type {EditorSchema, EditorSelection} from '..'
|
|
|
4
4
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
5
5
|
import * as utils from '../utils'
|
|
6
6
|
import {toPortableTextRange} from './ranges'
|
|
7
|
+
import {getFirstBlock, getLastBlock, getNodeBlock} from './slate-utils'
|
|
7
8
|
import {fromSlateValue} from './values'
|
|
8
9
|
|
|
9
10
|
export type EventPosition = {
|
|
@@ -31,6 +32,12 @@ export function getEventPosition({
|
|
|
31
32
|
return undefined
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
const block = getNodeBlock({
|
|
36
|
+
editor: slateEditor,
|
|
37
|
+
schema,
|
|
38
|
+
node,
|
|
39
|
+
})
|
|
40
|
+
|
|
34
41
|
const positionBlock = getEventPositionBlock({node, slateEditor, event})
|
|
35
42
|
const selection = getEventSelection({
|
|
36
43
|
schema,
|
|
@@ -38,13 +45,7 @@ export function getEventPosition({
|
|
|
38
45
|
event,
|
|
39
46
|
})
|
|
40
47
|
|
|
41
|
-
if (positionBlock && !selection && !Editor.isEditor(node)) {
|
|
42
|
-
const block = fromSlateValue([node], schema.block.name)?.at(0)
|
|
43
|
-
|
|
44
|
-
if (!block) {
|
|
45
|
-
return undefined
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
+
if (block && positionBlock && !selection && !Editor.isEditor(node)) {
|
|
48
49
|
return {
|
|
49
50
|
block: positionBlock,
|
|
50
51
|
isEditor: false,
|
|
@@ -65,6 +66,36 @@ export function getEventPosition({
|
|
|
65
66
|
return undefined
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
const focusBlockPath = selection.focus.path.at(0)
|
|
70
|
+
const focusBlockKey = utils.isKeyedSegment(focusBlockPath)
|
|
71
|
+
? focusBlockPath._key
|
|
72
|
+
: undefined
|
|
73
|
+
|
|
74
|
+
if (!focusBlockKey) {
|
|
75
|
+
return undefined
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (
|
|
79
|
+
utils.isSelectionCollapsed(selection) &&
|
|
80
|
+
block &&
|
|
81
|
+
focusBlockKey !== block._key
|
|
82
|
+
) {
|
|
83
|
+
return {
|
|
84
|
+
block: positionBlock,
|
|
85
|
+
isEditor: false,
|
|
86
|
+
selection: {
|
|
87
|
+
anchor: utils.getBlockStartPoint({
|
|
88
|
+
node: block,
|
|
89
|
+
path: [{_key: block._key}],
|
|
90
|
+
}),
|
|
91
|
+
focus: utils.getBlockEndPoint({
|
|
92
|
+
node: block,
|
|
93
|
+
path: [{_key: block._key}],
|
|
94
|
+
}),
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
68
99
|
return {
|
|
69
100
|
block: positionBlock,
|
|
70
101
|
isEditor: Editor.isEditor(node),
|
|
@@ -97,6 +128,32 @@ function getEventPositionBlock({
|
|
|
97
128
|
slateEditor: PortableTextSlateEditor
|
|
98
129
|
event: DragEvent | MouseEvent
|
|
99
130
|
}): EventPositionBlock | undefined {
|
|
131
|
+
const [firstBlock] = getFirstBlock({editor: slateEditor})
|
|
132
|
+
|
|
133
|
+
if (!firstBlock) {
|
|
134
|
+
return undefined
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const firstBlockElement = DOMEditor.toDOMNode(slateEditor, firstBlock)
|
|
138
|
+
const firstBlockRect = firstBlockElement.getBoundingClientRect()
|
|
139
|
+
|
|
140
|
+
if (event.pageY < firstBlockRect.top) {
|
|
141
|
+
return 'start'
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const [lastBlock] = getLastBlock({editor: slateEditor})
|
|
145
|
+
|
|
146
|
+
if (!lastBlock) {
|
|
147
|
+
return undefined
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const lastBlockElement = DOMEditor.toDOMNode(slateEditor, lastBlock)
|
|
151
|
+
const lastBlockRef = lastBlockElement.getBoundingClientRect()
|
|
152
|
+
|
|
153
|
+
if (event.pageY > lastBlockRef.bottom) {
|
|
154
|
+
return 'end'
|
|
155
|
+
}
|
|
156
|
+
|
|
100
157
|
const element = DOMEditor.toDOMNode(slateEditor, node)
|
|
101
158
|
const elementRect = element.getBoundingClientRect()
|
|
102
159
|
const top = elementRect.top
|
|
@@ -151,9 +208,11 @@ function getSlateRangeFromEvent(
|
|
|
151
208
|
)
|
|
152
209
|
|
|
153
210
|
if (position) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
211
|
+
try {
|
|
212
|
+
domRange = window.document.createRange()
|
|
213
|
+
domRange.setStart(position.offsetNode, position.offset)
|
|
214
|
+
domRange.setEnd(position.offsetNode, position.offset)
|
|
215
|
+
} catch {}
|
|
157
216
|
}
|
|
158
217
|
} else if (window.document.caretRangeFromPoint !== undefined) {
|
|
159
218
|
// Use WebKit-proprietary fallback method
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import {Editor, Node, type Path} from 'slate'
|
|
1
|
+
import {Editor, Element, Node, type Path} from 'slate'
|
|
2
|
+
import type {EditorSchema} from '../editor/define-schema'
|
|
2
3
|
import type {PortableTextSlateEditor} from '../types/editor'
|
|
4
|
+
import {fromSlateValue} from './values'
|
|
3
5
|
|
|
4
6
|
export function getFocusBlock({
|
|
5
7
|
editor,
|
|
@@ -39,18 +41,84 @@ export function getFocusChild({
|
|
|
39
41
|
: [undefined, undefined]
|
|
40
42
|
}
|
|
41
43
|
|
|
44
|
+
export function getFirstBlock({
|
|
45
|
+
editor,
|
|
46
|
+
}: {
|
|
47
|
+
editor: PortableTextSlateEditor
|
|
48
|
+
}): [node: Node, path: Path] | [undefined, undefined] {
|
|
49
|
+
const firstPoint = Editor.start(editor, [])
|
|
50
|
+
const firstBlockPath = firstPoint.path.at(0)
|
|
51
|
+
|
|
52
|
+
return firstBlockPath !== undefined
|
|
53
|
+
? (Editor.node(editor, [firstBlockPath]) ?? [undefined, undefined])
|
|
54
|
+
: [undefined, undefined]
|
|
55
|
+
}
|
|
56
|
+
|
|
42
57
|
export function getLastBlock({
|
|
43
58
|
editor,
|
|
44
59
|
}: {
|
|
45
60
|
editor: PortableTextSlateEditor
|
|
46
61
|
}): [node: Node, path: Path] | [undefined, undefined] {
|
|
47
|
-
const
|
|
62
|
+
const lastPoint = Editor.end(editor, [])
|
|
63
|
+
const lastBlockPath = lastPoint.path.at(0)
|
|
64
|
+
return lastBlockPath !== undefined
|
|
65
|
+
? (Editor.node(editor, [lastBlockPath]) ?? [undefined, undefined])
|
|
66
|
+
: [undefined, undefined]
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function getNodeBlock({
|
|
70
|
+
editor,
|
|
71
|
+
schema,
|
|
72
|
+
node,
|
|
73
|
+
}: {
|
|
74
|
+
editor: PortableTextSlateEditor
|
|
75
|
+
schema: EditorSchema
|
|
76
|
+
node: Node
|
|
77
|
+
}) {
|
|
78
|
+
if (Editor.isEditor(node)) {
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (isBlockElement(schema, node)) {
|
|
83
|
+
return elementToBlock({schema, element: node})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parent = Array.from(
|
|
48
87
|
Editor.nodes(editor, {
|
|
49
|
-
|
|
88
|
+
mode: 'highest',
|
|
50
89
|
at: [],
|
|
51
|
-
|
|
90
|
+
match: (n) =>
|
|
91
|
+
isBlockElement(schema, n) &&
|
|
92
|
+
n.children.some((child) => child._key === node._key),
|
|
52
93
|
}),
|
|
53
|
-
)
|
|
94
|
+
)
|
|
95
|
+
.at(0)
|
|
96
|
+
?.at(0)
|
|
97
|
+
|
|
98
|
+
return Element.isElement(parent)
|
|
99
|
+
? elementToBlock({
|
|
100
|
+
schema,
|
|
101
|
+
element: parent,
|
|
102
|
+
})
|
|
103
|
+
: undefined
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function elementToBlock({
|
|
107
|
+
schema,
|
|
108
|
+
element,
|
|
109
|
+
}: {
|
|
110
|
+
schema: EditorSchema
|
|
111
|
+
element: Element
|
|
112
|
+
}) {
|
|
113
|
+
return fromSlateValue([element], schema.block.name)?.at(0)
|
|
114
|
+
}
|
|
54
115
|
|
|
55
|
-
|
|
116
|
+
function isBlockElement(schema: EditorSchema, node: Node): node is Element {
|
|
117
|
+
return (
|
|
118
|
+
Element.isElement(node) &&
|
|
119
|
+
(schema.block.name === node._type ||
|
|
120
|
+
schema.blockObjects.some(
|
|
121
|
+
(blockObject) => blockObject.name === node._type,
|
|
122
|
+
))
|
|
123
|
+
)
|
|
56
124
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -8,6 +8,8 @@ export {blockOffsetsToSelection} from './util.block-offsets-to-selection'
|
|
|
8
8
|
export {childSelectionPointToBlockOffset} from './util.child-selection-point-to-block-offset'
|
|
9
9
|
export {getBlockEndPoint} from './util.get-block-end-point'
|
|
10
10
|
export {getBlockStartPoint} from './util.get-block-start-point'
|
|
11
|
+
export {getSelectionEndPoint} from './util.get-selection-end-point'
|
|
12
|
+
export {getSelectionStartPoint} from './util.get-selection-start-point'
|
|
11
13
|
export {getTextBlockText} from './util.get-text-block-text'
|
|
12
14
|
export {isEmptyTextBlock} from './util.is-empty-text-block'
|
|
13
15
|
export {isEqualSelectionPoints} from './util.is-equal-selection-points'
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {EditorSelection, EditorSelectionPoint} from '..'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export function getSelectionEndPoint<
|
|
7
|
+
TEditorSelection extends NonNullable<EditorSelection> | null,
|
|
8
|
+
TEditorSelectionPoint extends
|
|
9
|
+
EditorSelectionPoint | null = TEditorSelection extends NonNullable<EditorSelection>
|
|
10
|
+
? EditorSelectionPoint
|
|
11
|
+
: null,
|
|
12
|
+
>(selection: TEditorSelection): TEditorSelectionPoint {
|
|
13
|
+
if (!selection) {
|
|
14
|
+
return null as TEditorSelectionPoint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
selection.backward ? selection.anchor : selection.focus
|
|
19
|
+
) as TEditorSelectionPoint
|
|
20
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type {EditorSelection, EditorSelectionPoint} from '..'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @public
|
|
5
|
+
*/
|
|
6
|
+
export function getSelectionStartPoint<
|
|
7
|
+
TEditorSelection extends NonNullable<EditorSelection> | null,
|
|
8
|
+
TEditorSelectionPoint extends
|
|
9
|
+
EditorSelectionPoint | null = TEditorSelection extends NonNullable<EditorSelection>
|
|
10
|
+
? EditorSelectionPoint
|
|
11
|
+
: null,
|
|
12
|
+
>(selection: TEditorSelection): TEditorSelectionPoint {
|
|
13
|
+
if (!selection) {
|
|
14
|
+
return null as TEditorSelectionPoint
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
selection.backward ? selection.focus : selection.anchor
|
|
19
|
+
) as TEditorSelectionPoint
|
|
20
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type {KeyedSegment
|
|
1
|
+
import type {KeyedSegment} from '@sanity/types'
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* @public
|
|
5
5
|
*/
|
|
6
|
-
export function isKeyedSegment(segment:
|
|
6
|
+
export function isKeyedSegment(segment: unknown): segment is KeyedSegment {
|
|
7
7
|
return typeof segment === 'object' && segment !== null && '_key' in segment
|
|
8
8
|
}
|