@portabletext/editor 1.51.0 → 1.52.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 (37) hide show
  1. package/lib/_chunks-es/selector.is-selection-expanded.js +1 -1
  2. package/lib/behaviors/index.cjs.map +1 -1
  3. package/lib/behaviors/index.d.cts +26 -5
  4. package/lib/behaviors/index.d.ts +26 -5
  5. package/lib/behaviors/index.js.map +1 -1
  6. package/lib/index.cjs +434 -309
  7. package/lib/index.cjs.map +1 -1
  8. package/lib/index.d.cts +360 -31
  9. package/lib/index.d.ts +360 -31
  10. package/lib/index.js +455 -330
  11. package/lib/index.js.map +1 -1
  12. package/lib/plugins/index.d.cts +26 -5
  13. package/lib/plugins/index.d.ts +26 -5
  14. package/lib/selectors/index.d.cts +0 -14
  15. package/lib/selectors/index.d.ts +0 -14
  16. package/lib/utils/index.d.cts +0 -14
  17. package/lib/utils/index.d.ts +0 -14
  18. package/package.json +3 -3
  19. package/src/behaviors/behavior.abstract.delete.ts +0 -1
  20. package/src/behaviors/behavior.abstract.ts +0 -113
  21. package/src/behaviors/behavior.core.block-element.ts +9 -3
  22. package/src/behaviors/behavior.core.dnd.ts +328 -1
  23. package/src/behaviors/behavior.perform-event.ts +10 -0
  24. package/src/behaviors/behavior.types.action.ts +2 -0
  25. package/src/behaviors/behavior.types.event.ts +4 -0
  26. package/src/behaviors/behavior.types.guard.ts +2 -0
  27. package/src/converters/converter.portable-text.ts +2 -7
  28. package/src/converters/converter.text-html.ts +1 -3
  29. package/src/converters/converter.text-plain.ts +3 -5
  30. package/src/editor/Editable.tsx +6 -133
  31. package/src/editor/editor-machine.ts +15 -8
  32. package/src/editor/editor-selector.ts +0 -1
  33. package/src/editor/editor-snapshot.ts +0 -13
  34. package/src/internal-utils/create-test-snapshot.ts +0 -1
  35. package/src/internal-utils/event-position.ts +41 -27
  36. package/src/internal-utils/selection-elements.ts +108 -0
  37. package/src/operations/behavior.operation.decorator.add.ts +0 -1
@@ -23,18 +23,14 @@ import {
23
23
  type RenderElementProps,
24
24
  type RenderLeafProps,
25
25
  } from 'slate-react'
26
- import {getCompoundClientRect} from '../internal-utils/compound-client-rect'
27
26
  import {debugWithName} from '../internal-utils/debug'
28
- import {getDragSelection} from '../internal-utils/drag-selection'
29
27
  import {getEventPosition} from '../internal-utils/event-position'
30
28
  import {parseBlocks} from '../internal-utils/parse-blocks'
31
29
  import {toSlateRange} from '../internal-utils/ranges'
32
30
  import {normalizeSelection} from '../internal-utils/selection'
33
- import {getSelectionDomNodes} from '../internal-utils/selection-elements'
34
31
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
35
32
  import {fromSlateValue} from '../internal-utils/values'
36
33
  import {KEY_TO_VALUE_ELEMENT} from '../internal-utils/weakMaps'
37
- import * as selectors from '../selectors'
38
34
  import type {
39
35
  EditorSelection,
40
36
  OnCopyFn,
@@ -50,13 +46,10 @@ import type {
50
46
  ScrollSelectionIntoViewFunction,
51
47
  } from '../types/editor'
52
48
  import type {HotkeyOptions} from '../types/options'
53
- import {isSelectionCollapsed} from '../utils'
54
- import {getSelectionEndPoint} from '../utils/util.get-selection-end-point'
55
49
  import {RenderElement} from './components/render-element'
56
50
  import {RenderLeaf} from './components/render-leaf'
57
51
  import {RenderText, type RenderTextProps} from './components/render-text'
58
52
  import {EditorActorContext} from './editor-actor-context'
59
- import {getEditorSnapshot} from './editor-selector'
60
53
  import {usePortableTextEditor} from './hooks/usePortableTextEditor'
61
54
  import {createWithHotkeys} from './plugins/createWithHotKeys'
62
55
  import {PortableTextEditor} from './PortableTextEditor'
@@ -811,131 +804,9 @@ export const PortableTextEditable = forwardRef<
811
804
  return
812
805
  }
813
806
 
814
- const snapshot = getEditorSnapshot({
815
- editorActorSnapshot: editorActor.getSnapshot(),
816
- slateEditorInstance: slateEditor,
817
- })
818
- const dragSelection = getDragSelection({
819
- eventSelection: position.selection,
820
- snapshot,
821
- })
822
-
823
- const selectingEntireBlocks = selectors.isSelectingEntireBlocks({
824
- ...snapshot,
825
- context: {
826
- ...snapshot.context,
827
- selection: dragSelection,
828
- },
829
- })
830
-
831
- const dragGhost = document.createElement('div')
832
-
833
- const draggedDomNodes = getSelectionDomNodes({
834
- snapshot: {
835
- ...snapshot,
836
- context: {
837
- ...snapshot.context,
838
- selection: dragSelection,
839
- },
840
- },
841
- slateEditor,
842
- })
843
-
844
- if (selectingEntireBlocks) {
845
- // Clone the DOM Nodes so they won't be visually clipped by scroll-containers etc.
846
- const clonedBlockNodes = draggedDomNodes.blockNodes.map((node) =>
847
- node.cloneNode(true),
848
- )
849
-
850
- for (const block of clonedBlockNodes) {
851
- if (block instanceof HTMLElement) {
852
- block.style.position = 'relative'
853
- }
854
- dragGhost.appendChild(block)
855
- }
856
-
857
- // A custom drag ghost element can be configured using this data attribute
858
- const customGhost = dragGhost.querySelector(
859
- '[data-pt-drag-ghost-element]',
860
- )
861
- if (customGhost) {
862
- dragGhost.replaceChildren(customGhost)
863
- }
864
-
865
- // Setting the `data-dragged` attribute so the consumer can style the element while it’s dragged
866
- dragGhost.setAttribute('data-dragged', '')
867
-
868
- dragGhost.style.position = 'absolute'
869
- dragGhost.style.left = '-99999px'
870
- dragGhost.style.boxSizing = 'border-box'
871
- document.body.appendChild(dragGhost)
872
-
873
- if (customGhost) {
874
- const customGhostRect = customGhost.getBoundingClientRect()
875
- const x = event.clientX - customGhostRect.left
876
- const y = event.clientY - customGhostRect.top
877
- dragGhost.style.width = `${customGhostRect.width}px`
878
- dragGhost.style.height = `${customGhostRect.height}px`
879
- event.dataTransfer.setDragImage(dragGhost, x, y)
880
- } else {
881
- const blocksDomRect = getCompoundClientRect(
882
- draggedDomNodes.blockNodes,
883
- )
884
- const x = event.clientX - blocksDomRect.left
885
- const y = event.clientY - blocksDomRect.top
886
- dragGhost.style.width = `${blocksDomRect.width}px`
887
- dragGhost.style.height = `${blocksDomRect.height}px`
888
- event.dataTransfer.setDragImage(dragGhost, x, y)
889
- }
890
- } else {
891
- const clonedChildNodes = draggedDomNodes.childNodes.map((node) =>
892
- node.cloneNode(true),
893
- )
894
-
895
- for (const child of clonedChildNodes) {
896
- dragGhost.appendChild(child)
897
- }
898
-
899
- dragGhost.style.position = 'absolute'
900
- dragGhost.style.left = '-99999px'
901
- dragGhost.style.boxSizing = 'border-box'
902
- document.body.appendChild(dragGhost)
903
-
904
- const childrenDomRect = getCompoundClientRect(
905
- draggedDomNodes.childNodes,
906
- )
907
- const x = event.clientX - childrenDomRect.left
908
- const y = event.clientY - childrenDomRect.top
909
- dragGhost.style.width = `${childrenDomRect.width}px`
910
- dragGhost.style.height = `${childrenDomRect.height}px`
911
-
912
- event.dataTransfer.setDragImage(dragGhost, x, y)
913
- }
914
-
915
- // Select drag selection
916
- // If the selection is expanded then we just select the end of the
917
- // selection
918
- editorActor.send({
919
- type: 'behavior event',
920
- behaviorEvent: {
921
- type: 'select',
922
- at: isSelectionCollapsed(dragSelection)
923
- ? dragSelection
924
- : {
925
- anchor: getSelectionEndPoint(dragSelection),
926
- focus: getSelectionEndPoint(dragSelection),
927
- backward: false,
928
- },
929
- },
930
- editor: slateEditor,
931
- })
932
-
933
807
  editorActor.send({
934
808
  type: 'dragstart',
935
- origin: {
936
- selection: dragSelection,
937
- },
938
- ghost: dragGhost,
809
+ origin: position,
939
810
  })
940
811
 
941
812
  editorActor.send({
@@ -943,11 +814,11 @@ export const PortableTextEditable = forwardRef<
943
814
  behaviorEvent: {
944
815
  type: 'drag.dragstart',
945
816
  originEvent: {
817
+ clientX: event.clientX,
818
+ clientY: event.clientY,
946
819
  dataTransfer: event.dataTransfer,
947
820
  },
948
- position: {
949
- selection: dragSelection,
950
- },
821
+ position,
951
822
  },
952
823
  editor: slateEditor,
953
824
  })
@@ -1079,6 +950,7 @@ export const PortableTextEditable = forwardRef<
1079
950
  originEvent: {
1080
951
  dataTransfer: event.dataTransfer,
1081
952
  },
953
+ dragOrigin: editorActor.getSnapshot().context.internalDrag?.origin,
1082
954
  position,
1083
955
  },
1084
956
  editor: slateEditor,
@@ -1117,6 +989,7 @@ export const PortableTextEditable = forwardRef<
1117
989
  originEvent: {
1118
990
  dataTransfer: event.dataTransfer,
1119
991
  },
992
+ dragOrigin: editorActor.getSnapshot().context.internalDrag?.origin,
1120
993
  position,
1121
994
  },
1122
995
  editor: slateEditor,
@@ -114,10 +114,14 @@ export type InternalEditorEvent =
114
114
  }
115
115
  | MutationEvent
116
116
  | InternalPatchEvent
117
+ | {
118
+ type: 'set drag ghost'
119
+ ghost: HTMLElement
120
+ }
117
121
  | {
118
122
  type: 'dragstart'
119
- origin: Pick<EventPosition, 'selection'>
120
123
  ghost?: HTMLElement
124
+ origin: Pick<EventPosition, 'selection'>
121
125
  }
122
126
  | {type: 'dragend'}
123
127
  | {type: 'drop'}
@@ -148,9 +152,9 @@ export const editorMachine = setup({
148
152
  selection: EditorSelection
149
153
  initialValue: Array<PortableTextBlock> | undefined
150
154
  internalDrag?: {
151
- ghost?: HTMLElement
152
155
  origin: Pick<EventPosition, 'selection'>
153
156
  }
157
+ dragGhost?: HTMLElement
154
158
  slateEditor?: PortableTextSlateEditor
155
159
  },
156
160
  events: {} as InternalEditorEvent,
@@ -273,9 +277,9 @@ export const editorMachine = setup({
273
277
  keyGenerator: context.keyGenerator,
274
278
  readOnly: self.getSnapshot().matches({'edit mode': 'read only'}),
275
279
  schema: context.schema,
276
- internalDrag: context.internalDrag,
277
280
  }),
278
281
  nativeEvent: event.nativeEvent,
282
+ sendBack: (event) => self.send(event),
279
283
  })
280
284
  } catch (error) {
281
285
  console.error(
@@ -322,6 +326,9 @@ export const editorMachine = setup({
322
326
  emit(({event}) => ({...event, type: 'selection'})),
323
327
  ],
324
328
  },
329
+ 'set drag ghost': {
330
+ actions: assign({dragGhost: ({event}) => event.ghost}),
331
+ },
325
332
  },
326
333
  type: 'parallel',
327
334
  states: {
@@ -427,7 +434,6 @@ export const editorMachine = setup({
427
434
  actions: [
428
435
  assign({
429
436
  internalDrag: ({event}) => ({
430
- ghost: event.ghost,
431
437
  origin: event.origin,
432
438
  }),
433
439
  }),
@@ -495,20 +501,21 @@ export const editorMachine = setup({
495
501
  debug('exit: edit mode->editable->dragging internally')
496
502
  },
497
503
  ({context}) => {
498
- if (context.internalDrag?.ghost) {
504
+ if (context.dragGhost) {
499
505
  try {
500
- context.internalDrag.ghost.parentNode?.removeChild(
501
- context.internalDrag.ghost,
506
+ context.dragGhost.parentNode?.removeChild(
507
+ context.dragGhost,
502
508
  )
503
509
  } catch (error) {
504
510
  console.error(
505
511
  new Error(
506
- `Removing the internal drag ghost failed due to: ${error.message}`,
512
+ `Removing the drag ghost failed due to: ${error.message}`,
507
513
  ),
508
514
  )
509
515
  }
510
516
  }
511
517
  },
518
+ assign({dragGhost: undefined}),
512
519
  assign({internalDrag: undefined}),
513
520
  ],
514
521
  tags: ['dragging internally'],
@@ -86,7 +86,6 @@ export function getEditorSnapshot({
86
86
  markState: slateEditorInstance.markState,
87
87
  schema: editorActorSnapshot.context.schema,
88
88
  }),
89
- internalDrag: editorActorSnapshot.context.internalDrag,
90
89
  },
91
90
  }
92
91
  }
@@ -1,6 +1,5 @@
1
1
  import type {PortableTextBlock} from '@sanity/types'
2
2
  import type {Converter} from '../converters/converter.types'
3
- import type {EventPosition} from '../internal-utils/event-position'
4
3
  import {slateRangeToSelection} from '../internal-utils/slate-utils'
5
4
  import type {EditorSelection, PortableTextSlateEditor} from '../types/editor'
6
5
  import type {EditorSchema} from './editor-schema'
@@ -31,11 +30,6 @@ export type EditorSnapshot = {
31
30
  beta: {
32
31
  activeAnnotations: Array<string>
33
32
  activeDecorators: Array<string>
34
- internalDrag:
35
- | {
36
- origin: Pick<EventPosition, 'selection'>
37
- }
38
- | undefined
39
33
  }
40
34
  }
41
35
 
@@ -45,18 +39,12 @@ export function createEditorSnapshot({
45
39
  keyGenerator,
46
40
  readOnly,
47
41
  schema,
48
- internalDrag,
49
42
  }: {
50
43
  converters: Array<Converter>
51
44
  editor: PortableTextSlateEditor
52
45
  keyGenerator: () => string
53
46
  readOnly: boolean
54
47
  schema: EditorSchema
55
- internalDrag:
56
- | {
57
- origin: Pick<EventPosition, 'selection'>
58
- }
59
- | undefined
60
48
  }) {
61
49
  const selection = editor.selection
62
50
  ? slateRangeToSelection({
@@ -87,7 +75,6 @@ export function createEditorSnapshot({
87
75
  markState: editor.markState,
88
76
  schema,
89
77
  }),
90
- internalDrag,
91
78
  },
92
79
  } satisfies EditorSnapshot
93
80
  }
@@ -19,7 +19,6 @@ export function createTestSnapshot(snapshot: {
19
19
  beta: {
20
20
  activeAnnotations: snapshot.beta?.activeAnnotations ?? [],
21
21
  activeDecorators: snapshot.beta?.activeDecorators ?? [],
22
- internalDrag: undefined,
23
22
  },
24
23
  }
25
24
  }
@@ -35,79 +35,93 @@ export function getEventPosition({
35
35
  return undefined
36
36
  }
37
37
 
38
- const node = getEventNode({slateEditor, event})
38
+ const eventNode = getEventNode({slateEditor, event})
39
39
 
40
- if (!node) {
40
+ if (!eventNode) {
41
41
  return undefined
42
42
  }
43
43
 
44
- const block = getNodeBlock({
44
+ const eventBlock = getNodeBlock({
45
45
  editor: slateEditor,
46
46
  schema: editorActor.getSnapshot().context.schema,
47
- node,
47
+ node: eventNode,
48
48
  })
49
-
50
- const positionBlock = getEventPositionBlock({node, slateEditor, event})
51
- const selection = getEventSelection({
49
+ const eventPositionBlock = getEventPositionBlock({
50
+ node: eventNode,
51
+ slateEditor,
52
+ event,
53
+ })
54
+ const eventSelection = getEventSelection({
52
55
  schema: editorActor.getSnapshot().context.schema,
53
56
  slateEditor,
54
57
  event,
55
58
  })
56
59
 
57
- if (block && positionBlock && !selection && !Editor.isEditor(node)) {
60
+ if (
61
+ eventBlock &&
62
+ eventPositionBlock &&
63
+ !eventSelection &&
64
+ !Editor.isEditor(eventNode)
65
+ ) {
66
+ // If we for some reason can't find the event selection, then we default to
67
+ // selecting the entire block that the event originates from.
58
68
  return {
59
- block: positionBlock,
69
+ block: eventPositionBlock,
60
70
  isEditor: false,
61
71
  selection: {
62
72
  anchor: utils.getBlockStartPoint({
63
73
  context: editorActor.getSnapshot().context,
64
74
  block: {
65
- node: block,
66
- path: [{_key: block._key}],
75
+ node: eventBlock,
76
+ path: [{_key: eventBlock._key}],
67
77
  },
68
78
  }),
69
79
  focus: utils.getBlockEndPoint({
70
80
  context: editorActor.getSnapshot().context,
71
81
  block: {
72
- node: block,
73
- path: [{_key: block._key}],
82
+ node: eventBlock,
83
+ path: [{_key: eventBlock._key}],
74
84
  },
75
85
  }),
76
86
  },
77
87
  }
78
88
  }
79
89
 
80
- if (!positionBlock || !selection) {
90
+ if (!eventPositionBlock || !eventSelection) {
81
91
  return undefined
82
92
  }
83
93
 
84
- const focusBlockKey = getBlockKeyFromSelectionPoint(selection.focus)
94
+ const eventSelectionFocusBlockKey = getBlockKeyFromSelectionPoint(
95
+ eventSelection.focus,
96
+ )
85
97
 
86
- if (focusBlockKey === undefined) {
98
+ if (eventSelectionFocusBlockKey === undefined) {
87
99
  return undefined
88
100
  }
89
101
 
90
102
  if (
91
- utils.isSelectionCollapsed(selection) &&
92
- block &&
93
- focusBlockKey !== block._key
103
+ utils.isSelectionCollapsed(eventSelection) &&
104
+ eventBlock &&
105
+ eventSelectionFocusBlockKey !== eventBlock._key
94
106
  ) {
107
+ // If the event block and event selection somehow don't match, then the
108
+ // event block takes precedence.
95
109
  return {
96
- block: positionBlock,
110
+ block: eventPositionBlock,
97
111
  isEditor: false,
98
112
  selection: {
99
113
  anchor: utils.getBlockStartPoint({
100
114
  context: editorActor.getSnapshot().context,
101
115
  block: {
102
- node: block,
103
- path: [{_key: block._key}],
116
+ node: eventBlock,
117
+ path: [{_key: eventBlock._key}],
104
118
  },
105
119
  }),
106
120
  focus: utils.getBlockEndPoint({
107
121
  context: editorActor.getSnapshot().context,
108
122
  block: {
109
- node: block,
110
- path: [{_key: block._key}],
123
+ node: eventBlock,
124
+ path: [{_key: eventBlock._key}],
111
125
  },
112
126
  }),
113
127
  },
@@ -115,9 +129,9 @@ export function getEventPosition({
115
129
  }
116
130
 
117
131
  return {
118
- block: positionBlock,
119
- isEditor: Editor.isEditor(node),
120
- selection,
132
+ block: eventPositionBlock,
133
+ isEditor: Editor.isEditor(eventNode),
134
+ selection: eventSelection,
121
135
  }
122
136
  }
123
137
 
@@ -1,9 +1,96 @@
1
1
  import {Editor} from 'slate'
2
2
  import {DOMEditor} from 'slate-dom'
3
3
  import type {EditorSnapshot} from '..'
4
+ import type {BehaviorEvent} from '../behaviors'
5
+ import type {PickFromUnion} from '../type-utils'
4
6
  import type {PortableTextSlateEditor} from '../types/editor'
5
7
  import {toSlateRange} from './ranges'
6
8
 
9
+ export type EditorDom = {
10
+ getBlockNodes: (snapshot: EditorSnapshot) => Array<Node>
11
+ getChildNodes: (snapshot: EditorSnapshot) => Array<Node>
12
+ /**
13
+ * Let the Editor set the drag ghost. This is to be sure that it will get
14
+ * properly removed again when the drag ends.
15
+ */
16
+ setDragGhost: ({
17
+ event,
18
+ ghost,
19
+ }: {
20
+ event: PickFromUnion<BehaviorEvent, 'type', 'drag.dragstart'>
21
+ ghost: {
22
+ element: HTMLElement
23
+ x: number
24
+ y: number
25
+ }
26
+ }) => void
27
+ }
28
+
29
+ export function createEditorDom(
30
+ sendBack: (event: {type: 'set drag ghost'; ghost: HTMLElement}) => void,
31
+ slateEditor: PortableTextSlateEditor,
32
+ ): EditorDom {
33
+ return {
34
+ getBlockNodes: (snapshot) => getBlockNodes(slateEditor, snapshot),
35
+ getChildNodes: (snapshot) => getChildNodes(slateEditor, snapshot),
36
+ setDragGhost: ({event, ghost}) => setDragGhost({sendBack, event, ghost}),
37
+ }
38
+ }
39
+
40
+ function getBlockNodes(
41
+ slateEditor: PortableTextSlateEditor,
42
+ snapshot: EditorSnapshot,
43
+ ) {
44
+ if (!snapshot.context.selection) {
45
+ return []
46
+ }
47
+
48
+ const range = toSlateRange(snapshot.context.selection, slateEditor)
49
+
50
+ if (!range) {
51
+ return []
52
+ }
53
+
54
+ const blockEntries = Array.from(
55
+ Editor.nodes(slateEditor, {
56
+ at: range,
57
+ mode: 'highest',
58
+ match: (n) => !Editor.isEditor(n),
59
+ }),
60
+ )
61
+
62
+ return blockEntries.map(([blockNode]) =>
63
+ DOMEditor.toDOMNode(slateEditor, blockNode),
64
+ )
65
+ }
66
+
67
+ function getChildNodes(
68
+ slateEditor: PortableTextSlateEditor,
69
+ snapshot: EditorSnapshot,
70
+ ) {
71
+ if (!snapshot.context.selection) {
72
+ return []
73
+ }
74
+
75
+ const range = toSlateRange(snapshot.context.selection, slateEditor)
76
+
77
+ if (!range) {
78
+ return []
79
+ }
80
+
81
+ const childEntries = Array.from(
82
+ Editor.nodes(slateEditor, {
83
+ at: range,
84
+ mode: 'lowest',
85
+ match: (n) => !Editor.isEditor(n),
86
+ }),
87
+ )
88
+
89
+ return childEntries.map(([childNode]) =>
90
+ DOMEditor.toDOMNode(slateEditor, childNode),
91
+ )
92
+ }
93
+
7
94
  export type SelectionDomNodes = {
8
95
  blockNodes: Array<Node>
9
96
  childNodes: Array<Node>
@@ -59,3 +146,24 @@ export function getSelectionDomNodes({
59
146
  ),
60
147
  }
61
148
  }
149
+
150
+ function setDragGhost({
151
+ sendBack,
152
+ event,
153
+ ghost,
154
+ }: {
155
+ sendBack: (event: {type: 'set drag ghost'; ghost: HTMLElement}) => void
156
+ event: PickFromUnion<BehaviorEvent, 'type', 'drag.dragstart'>
157
+ ghost: {
158
+ element: HTMLElement
159
+ x: number
160
+ y: number
161
+ }
162
+ }) {
163
+ event.originEvent.dataTransfer.setDragImage(ghost.element, ghost.x, ghost.y)
164
+
165
+ sendBack({
166
+ type: 'set drag ghost',
167
+ ghost: ghost.element,
168
+ })
169
+ }
@@ -111,7 +111,6 @@ export const decoratorAddOperationImplementation: BehaviorOperationImplementatio
111
111
  beta: {
112
112
  activeAnnotations: [],
113
113
  activeDecorators: [],
114
- internalDrag: undefined,
115
114
  },
116
115
  context: {
117
116
  converters: [],