@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.
Files changed (34) hide show
  1. package/lib/_chunks-cjs/editor-provider.cjs +72 -34
  2. package/lib/_chunks-cjs/editor-provider.cjs.map +1 -1
  3. package/lib/_chunks-cjs/util.is-selection-collapsed.cjs +10 -0
  4. package/lib/_chunks-cjs/util.is-selection-collapsed.cjs.map +1 -0
  5. package/lib/_chunks-cjs/util.slice-blocks.cjs.map +1 -1
  6. package/lib/_chunks-es/editor-provider.js +73 -35
  7. package/lib/_chunks-es/editor-provider.js.map +1 -1
  8. package/lib/_chunks-es/util.is-selection-collapsed.js +11 -0
  9. package/lib/_chunks-es/util.is-selection-collapsed.js.map +1 -0
  10. package/lib/_chunks-es/util.slice-blocks.js.map +1 -1
  11. package/lib/index.cjs +307 -144
  12. package/lib/index.cjs.map +1 -1
  13. package/lib/index.js +309 -145
  14. package/lib/index.js.map +1 -1
  15. package/lib/utils/index.cjs +7 -5
  16. package/lib/utils/index.cjs.map +1 -1
  17. package/lib/utils/index.d.cts +23 -2
  18. package/lib/utils/index.d.ts +23 -2
  19. package/lib/utils/index.js +6 -3
  20. package/lib/utils/index.js.map +1 -1
  21. package/package.json +4 -4
  22. package/src/behavior-actions/behavior.action.insert-blocks.ts +5 -1
  23. package/src/converters/converter.text-plain.ts +24 -11
  24. package/src/editor/Editable.tsx +336 -223
  25. package/src/editor/components/drop-indicator.tsx +4 -1
  26. package/src/internal-utils/drag-selection.test.ts +74 -1
  27. package/src/internal-utils/drag-selection.ts +20 -4
  28. package/src/internal-utils/dragging-on-drag-origin.ts +22 -0
  29. package/src/internal-utils/event-position.ts +69 -10
  30. package/src/internal-utils/slate-utils.ts +74 -6
  31. package/src/utils/index.ts +2 -0
  32. package/src/utils/util.get-selection-end-point.ts +20 -0
  33. package/src/utils/util.get-selection-start-point.ts +20 -0
  34. 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: 'bar',
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 && selectionEndBlock) {
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: utils.getBlockStartPoint(selectionStartBlock),
60
- focus: utils.getBlockEndPoint(selectionEndBlock),
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
- domRange = window.document.createRange()
155
- domRange.setStart(position.offsetNode, position.offset)
156
- domRange.setEnd(position.offsetNode, position.offset)
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 lastBlock = Array.from(
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
- match: (n) => !Editor.isEditor(n),
88
+ mode: 'highest',
50
89
  at: [],
51
- reverse: true,
90
+ match: (n) =>
91
+ isBlockElement(schema, n) &&
92
+ n.children.some((child) => child._key === node._key),
52
93
  }),
53
- ).at(0)
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
- return lastBlock ?? [undefined, undefined]
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
  }
@@ -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, PathSegment} from '@sanity/types'
1
+ import type {KeyedSegment} from '@sanity/types'
2
2
 
3
3
  /**
4
4
  * @public
5
5
  */
6
- export function isKeyedSegment(segment: PathSegment): segment is KeyedSegment {
6
+ export function isKeyedSegment(segment: unknown): segment is KeyedSegment {
7
7
  return typeof segment === 'object' && segment !== null && '_key' in segment
8
8
  }