@portabletext/editor 0.0.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 (97) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +3 -0
  3. package/lib/index.d.mts +911 -0
  4. package/lib/index.d.ts +911 -0
  5. package/lib/index.esm.js +4896 -0
  6. package/lib/index.esm.js.map +1 -0
  7. package/lib/index.js +4874 -0
  8. package/lib/index.js.map +1 -0
  9. package/lib/index.mjs +4896 -0
  10. package/lib/index.mjs.map +1 -0
  11. package/package.json +119 -0
  12. package/src/editor/Editable.tsx +683 -0
  13. package/src/editor/PortableTextEditor.tsx +308 -0
  14. package/src/editor/__tests__/PortableTextEditor.test.tsx +386 -0
  15. package/src/editor/__tests__/PortableTextEditorTester.tsx +116 -0
  16. package/src/editor/__tests__/RangeDecorations.test.tsx +115 -0
  17. package/src/editor/__tests__/handleClick.test.tsx +218 -0
  18. package/src/editor/__tests__/pteWarningsSelfSolving.test.tsx +389 -0
  19. package/src/editor/__tests__/utils.ts +39 -0
  20. package/src/editor/components/DraggableBlock.tsx +287 -0
  21. package/src/editor/components/Element.tsx +279 -0
  22. package/src/editor/components/Leaf.tsx +288 -0
  23. package/src/editor/components/SlateContainer.tsx +81 -0
  24. package/src/editor/components/Synchronizer.tsx +190 -0
  25. package/src/editor/hooks/usePortableTextEditor.ts +23 -0
  26. package/src/editor/hooks/usePortableTextEditorKeyGenerator.ts +24 -0
  27. package/src/editor/hooks/usePortableTextEditorSelection.ts +22 -0
  28. package/src/editor/hooks/usePortableTextEditorValue.ts +16 -0
  29. package/src/editor/hooks/usePortableTextReadOnly.ts +20 -0
  30. package/src/editor/hooks/useSyncValue.test.tsx +125 -0
  31. package/src/editor/hooks/useSyncValue.ts +372 -0
  32. package/src/editor/nodes/DefaultAnnotation.tsx +16 -0
  33. package/src/editor/nodes/DefaultObject.tsx +15 -0
  34. package/src/editor/nodes/index.ts +189 -0
  35. package/src/editor/plugins/__tests__/withEditableAPIDelete.test.tsx +244 -0
  36. package/src/editor/plugins/__tests__/withEditableAPIGetFragment.test.tsx +142 -0
  37. package/src/editor/plugins/__tests__/withEditableAPIInsert.test.tsx +346 -0
  38. package/src/editor/plugins/__tests__/withEditableAPISelectionsOverlapping.test.tsx +162 -0
  39. package/src/editor/plugins/__tests__/withHotkeys.test.tsx +212 -0
  40. package/src/editor/plugins/__tests__/withInsertBreak.test.tsx +204 -0
  41. package/src/editor/plugins/__tests__/withPlaceholderBlock.test.tsx +133 -0
  42. package/src/editor/plugins/__tests__/withPortableTextLists.test.tsx +65 -0
  43. package/src/editor/plugins/__tests__/withPortableTextMarkModel.test.tsx +1377 -0
  44. package/src/editor/plugins/__tests__/withPortableTextSelections.test.tsx +91 -0
  45. package/src/editor/plugins/__tests__/withUndoRedo.test.tsx +115 -0
  46. package/src/editor/plugins/createWithEditableAPI.ts +573 -0
  47. package/src/editor/plugins/createWithHotKeys.ts +304 -0
  48. package/src/editor/plugins/createWithInsertBreak.ts +45 -0
  49. package/src/editor/plugins/createWithInsertData.ts +359 -0
  50. package/src/editor/plugins/createWithMaxBlocks.ts +24 -0
  51. package/src/editor/plugins/createWithObjectKeys.ts +63 -0
  52. package/src/editor/plugins/createWithPatches.ts +274 -0
  53. package/src/editor/plugins/createWithPlaceholderBlock.ts +36 -0
  54. package/src/editor/plugins/createWithPortableTextBlockStyle.ts +91 -0
  55. package/src/editor/plugins/createWithPortableTextLists.ts +160 -0
  56. package/src/editor/plugins/createWithPortableTextMarkModel.ts +441 -0
  57. package/src/editor/plugins/createWithPortableTextSelections.ts +65 -0
  58. package/src/editor/plugins/createWithSchemaTypes.ts +76 -0
  59. package/src/editor/plugins/createWithUndoRedo.ts +494 -0
  60. package/src/editor/plugins/createWithUtils.ts +81 -0
  61. package/src/editor/plugins/index.ts +155 -0
  62. package/src/index.ts +11 -0
  63. package/src/patch/PatchEvent.ts +33 -0
  64. package/src/patch/applyPatch.ts +29 -0
  65. package/src/patch/array.ts +89 -0
  66. package/src/patch/arrayInsert.ts +27 -0
  67. package/src/patch/object.ts +39 -0
  68. package/src/patch/patches.ts +53 -0
  69. package/src/patch/primitive.ts +43 -0
  70. package/src/patch/string.ts +51 -0
  71. package/src/types/editor.ts +576 -0
  72. package/src/types/options.ts +17 -0
  73. package/src/types/patch.ts +65 -0
  74. package/src/types/slate.ts +25 -0
  75. package/src/utils/__tests__/dmpToOperations.test.ts +181 -0
  76. package/src/utils/__tests__/operationToPatches.test.ts +421 -0
  77. package/src/utils/__tests__/patchToOperations.test.ts +293 -0
  78. package/src/utils/__tests__/ranges.test.ts +18 -0
  79. package/src/utils/__tests__/valueNormalization.test.tsx +62 -0
  80. package/src/utils/__tests__/values.test.ts +253 -0
  81. package/src/utils/applyPatch.ts +407 -0
  82. package/src/utils/bufferUntil.ts +15 -0
  83. package/src/utils/debug.ts +12 -0
  84. package/src/utils/getPortableTextMemberSchemaTypes.ts +100 -0
  85. package/src/utils/operationToPatches.ts +357 -0
  86. package/src/utils/patches.ts +36 -0
  87. package/src/utils/paths.ts +60 -0
  88. package/src/utils/ranges.ts +77 -0
  89. package/src/utils/schema.ts +8 -0
  90. package/src/utils/selection.ts +65 -0
  91. package/src/utils/ucs2Indices.ts +67 -0
  92. package/src/utils/validateValue.ts +394 -0
  93. package/src/utils/values.ts +208 -0
  94. package/src/utils/weakMaps.ts +24 -0
  95. package/src/utils/withChanges.ts +25 -0
  96. package/src/utils/withPreserveKeys.ts +14 -0
  97. package/src/utils/withoutPatching.ts +14 -0
@@ -0,0 +1,389 @@
1
+ import {describe, expect, it, jest} from '@jest/globals'
2
+ import {type PortableTextBlock} from '@sanity/types'
3
+ import {render, waitFor} from '@testing-library/react'
4
+ import {createRef, type RefObject} from 'react'
5
+
6
+ import {PortableTextEditor} from '../PortableTextEditor'
7
+ import {PortableTextEditorTester, schemaType} from './PortableTextEditorTester'
8
+
9
+ describe('when PTE would display warnings, instead it self solves', () => {
10
+ it('when child at index is missing required _key in block with _key', async () => {
11
+ const editorRef: RefObject<PortableTextEditor> = createRef()
12
+ const initialValue = [
13
+ {
14
+ _key: 'abc',
15
+ _type: 'myTestBlockType',
16
+ children: [
17
+ {
18
+ _type: 'span',
19
+ marks: [],
20
+ text: 'Hello with a new key',
21
+ },
22
+ ],
23
+ markDefs: [],
24
+ style: 'normal',
25
+ },
26
+ ]
27
+
28
+ const onChange = jest.fn()
29
+ render(
30
+ <PortableTextEditorTester
31
+ onChange={onChange}
32
+ ref={editorRef}
33
+ schemaType={schemaType}
34
+ value={initialValue}
35
+ />,
36
+ )
37
+ await waitFor(() => {
38
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
39
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
40
+ })
41
+ await waitFor(() => {
42
+ if (editorRef.current) {
43
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
44
+ {
45
+ _key: 'abc',
46
+ _type: 'myTestBlockType',
47
+ children: [
48
+ {
49
+ _key: '4',
50
+ _type: 'span',
51
+ text: 'Hello with a new key',
52
+ marks: [],
53
+ },
54
+ ],
55
+ markDefs: [],
56
+ style: 'normal',
57
+ },
58
+ ])
59
+ }
60
+ })
61
+ })
62
+
63
+ it('allows missing .markDefs', async () => {
64
+ const editorRef: RefObject<PortableTextEditor> = createRef()
65
+ const initialValue = [
66
+ {
67
+ _key: 'abc',
68
+ _type: 'myTestBlockType',
69
+ children: [
70
+ {
71
+ _key: 'def',
72
+ _type: 'span',
73
+ marks: [],
74
+ text: 'No markDefs',
75
+ },
76
+ ],
77
+ style: 'normal',
78
+ },
79
+ ]
80
+
81
+ const onChange = jest.fn()
82
+ render(
83
+ <PortableTextEditorTester
84
+ onChange={onChange}
85
+ ref={editorRef}
86
+ schemaType={schemaType}
87
+ value={initialValue}
88
+ />,
89
+ )
90
+ await waitFor(() => {
91
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
92
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
93
+ })
94
+ await waitFor(() => {
95
+ if (editorRef.current) {
96
+ PortableTextEditor.focus(editorRef.current)
97
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
98
+ {
99
+ _key: 'abc',
100
+ _type: 'myTestBlockType',
101
+ children: [
102
+ {
103
+ _key: 'def',
104
+ _type: 'span',
105
+ text: 'No markDefs',
106
+ marks: [],
107
+ },
108
+ ],
109
+ style: 'normal',
110
+ },
111
+ ])
112
+ }
113
+ })
114
+ })
115
+
116
+ it('adds missing .children', async () => {
117
+ const editorRef: RefObject<PortableTextEditor> = createRef()
118
+ const initialValue = [
119
+ {
120
+ _key: 'abc',
121
+ _type: 'myTestBlockType',
122
+ style: 'normal',
123
+ markDefs: [],
124
+ },
125
+ {
126
+ _key: 'def',
127
+ _type: 'myTestBlockType',
128
+ style: 'normal',
129
+ children: [],
130
+ markDefs: [],
131
+ },
132
+ ]
133
+
134
+ const onChange = jest.fn()
135
+ render(
136
+ <PortableTextEditorTester
137
+ onChange={onChange}
138
+ ref={editorRef}
139
+ schemaType={schemaType}
140
+ value={initialValue}
141
+ />,
142
+ )
143
+ await waitFor(() => {
144
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
145
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
146
+ })
147
+ await waitFor(() => {
148
+ if (editorRef.current) {
149
+ PortableTextEditor.focus(editorRef.current)
150
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
151
+ {
152
+ _key: 'abc',
153
+ _type: 'myTestBlockType',
154
+ children: [
155
+ {
156
+ _key: '5',
157
+ _type: 'span',
158
+ text: '',
159
+ marks: [],
160
+ },
161
+ ],
162
+ markDefs: [],
163
+ style: 'normal',
164
+ },
165
+ {
166
+ _key: 'def',
167
+ _type: 'myTestBlockType',
168
+ children: [
169
+ {
170
+ _key: '6',
171
+ _type: 'span',
172
+ text: '',
173
+ marks: [],
174
+ },
175
+ ],
176
+ markDefs: [],
177
+ style: 'normal',
178
+ },
179
+ ])
180
+ }
181
+ })
182
+ })
183
+
184
+ it('removes orphaned marks', async () => {
185
+ const editorRef: RefObject<PortableTextEditor> = createRef()
186
+ const initialValue = [
187
+ {
188
+ _key: 'abc',
189
+ _type: 'myTestBlockType',
190
+ style: 'normal',
191
+ markDefs: [],
192
+ children: [
193
+ {
194
+ _key: 'def',
195
+ _type: 'span',
196
+ marks: ['ghi'],
197
+ text: 'Hello',
198
+ },
199
+ ],
200
+ },
201
+ ]
202
+
203
+ const onChange = jest.fn()
204
+ render(
205
+ <PortableTextEditorTester
206
+ onChange={onChange}
207
+ ref={editorRef}
208
+ schemaType={schemaType}
209
+ value={initialValue}
210
+ />,
211
+ )
212
+ await waitFor(() => {
213
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
214
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
215
+ })
216
+ await waitFor(() => {
217
+ if (editorRef.current) {
218
+ PortableTextEditor.focus(editorRef.current)
219
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
220
+ {
221
+ _key: 'abc',
222
+ _type: 'myTestBlockType',
223
+ children: [
224
+ {
225
+ _key: 'def',
226
+ _type: 'span',
227
+ text: 'Hello',
228
+ marks: [],
229
+ },
230
+ ],
231
+ markDefs: [],
232
+ style: 'normal',
233
+ },
234
+ ])
235
+ }
236
+ })
237
+ })
238
+
239
+ it('removes orphaned marksDefs', async () => {
240
+ const editorRef: RefObject<PortableTextEditor> = createRef()
241
+ const initialValue = [
242
+ {
243
+ _key: 'abc',
244
+ _type: 'myTestBlockType',
245
+ style: 'normal',
246
+ markDefs: [
247
+ {
248
+ _key: 'ghi',
249
+ _type: 'link',
250
+ href: 'https://sanity.io',
251
+ },
252
+ ],
253
+ children: [
254
+ {
255
+ _key: 'def',
256
+ _type: 'span',
257
+ marks: [],
258
+ text: 'Hello',
259
+ },
260
+ ],
261
+ },
262
+ ]
263
+
264
+ const onChange = jest.fn()
265
+ render(
266
+ <PortableTextEditorTester
267
+ onChange={onChange}
268
+ ref={editorRef}
269
+ schemaType={schemaType}
270
+ value={initialValue}
271
+ />,
272
+ )
273
+ await waitFor(() => {
274
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
275
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
276
+ })
277
+ await waitFor(() => {
278
+ if (editorRef.current) {
279
+ PortableTextEditor.focus(editorRef.current)
280
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
281
+ {
282
+ _key: 'abc',
283
+ _type: 'myTestBlockType',
284
+ children: [
285
+ {
286
+ _key: 'def',
287
+ _type: 'span',
288
+ text: 'Hello',
289
+ marks: [],
290
+ },
291
+ ],
292
+ markDefs: [],
293
+ style: 'normal',
294
+ },
295
+ ])
296
+ }
297
+ })
298
+ })
299
+
300
+ it('allows missing .markDefs', async () => {
301
+ const editorRef: RefObject<PortableTextEditor> = createRef()
302
+ const initialValue = [
303
+ {
304
+ _key: 'abc',
305
+ _type: 'myTestBlockType',
306
+ children: [
307
+ {
308
+ _key: 'def',
309
+ _type: 'span',
310
+ marks: [],
311
+ text: 'No markDefs',
312
+ },
313
+ ],
314
+ style: 'normal',
315
+ },
316
+ ]
317
+
318
+ const onChange = jest.fn()
319
+ render(
320
+ <PortableTextEditorTester
321
+ onChange={onChange}
322
+ ref={editorRef}
323
+ schemaType={schemaType}
324
+ value={initialValue}
325
+ />,
326
+ )
327
+ await waitFor(() => {
328
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
329
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
330
+ })
331
+ await waitFor(() => {
332
+ if (editorRef.current) {
333
+ PortableTextEditor.focus(editorRef.current)
334
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
335
+ {
336
+ _key: 'abc',
337
+ _type: 'myTestBlockType',
338
+ children: [
339
+ {
340
+ _key: 'def',
341
+ _type: 'span',
342
+ text: 'No markDefs',
343
+ marks: [],
344
+ },
345
+ ],
346
+ style: 'normal',
347
+ },
348
+ ])
349
+ }
350
+ })
351
+ })
352
+
353
+ it('allows empty array of blocks', async () => {
354
+ const editorRef: RefObject<PortableTextEditor> = createRef()
355
+ const initialValue = [] as PortableTextBlock[]
356
+
357
+ const onChange = jest.fn()
358
+ render(
359
+ <PortableTextEditorTester
360
+ onChange={onChange}
361
+ ref={editorRef}
362
+ schemaType={schemaType}
363
+ value={initialValue}
364
+ />,
365
+ )
366
+ await waitFor(() => {
367
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
368
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
369
+ })
370
+ await waitFor(() => {
371
+ if (editorRef.current) {
372
+ PortableTextEditor.focus(editorRef.current)
373
+ expect(PortableTextEditor.getValue(editorRef.current)).toEqual([
374
+ {
375
+ _key: '5',
376
+ _type: 'myTestBlockType',
377
+ children: [{_key: '4', _type: 'span', marks: [], text: ''}],
378
+ markDefs: [],
379
+ style: 'normal',
380
+ },
381
+ ])
382
+ }
383
+ })
384
+ await waitFor(() => {
385
+ expect(onChange).toHaveBeenCalledWith({type: 'value', value: initialValue})
386
+ expect(onChange).toHaveBeenCalledWith({type: 'ready'})
387
+ })
388
+ })
389
+ })
@@ -0,0 +1,39 @@
1
+ // This utils are inspired from https://github.dev/mwood23/slate-test-utils/blob/master/src/buildTestHarness.tsx
2
+ import {act, fireEvent, type render} from '@testing-library/react'
3
+ import {parseHotkey} from 'is-hotkey-esm'
4
+
5
+ export async function triggerKeyboardEvent(hotkey: string, element: Element): Promise<void> {
6
+ return act(async () => {
7
+ const eventProps = parseHotkey(hotkey)
8
+ const values = hotkey.split('+')
9
+
10
+ fireEvent(
11
+ element,
12
+ new window.KeyboardEvent('keydown', {
13
+ key: values[values.length - 1],
14
+ code: `${eventProps.which}`,
15
+ keyCode: eventProps.which,
16
+ bubbles: true,
17
+ ...eventProps,
18
+ }),
19
+ )
20
+ })
21
+ }
22
+
23
+ export async function getEditableElement(component: ReturnType<typeof render>): Promise<Element> {
24
+ await act(async () => component)
25
+ const element = component.container.querySelector('[data-slate-editor="true"]')
26
+ if (!element) {
27
+ throw new Error('Could not find element')
28
+ }
29
+ /**
30
+ * Manually add this because JSDom doesn't implement this and Slate checks for it
31
+ * internally before doing stuff.
32
+ *
33
+ * https://github.com/jsdom/jsdom/issues/1670
34
+ */
35
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
36
+ // @ts-ignore
37
+ element.isContentEditable = true
38
+ return element
39
+ }
@@ -0,0 +1,287 @@
1
+ import {
2
+ type DragEvent,
3
+ type MutableRefObject,
4
+ type ReactNode,
5
+ useCallback,
6
+ useEffect,
7
+ useMemo,
8
+ useRef,
9
+ useState,
10
+ } from 'react'
11
+ import {Editor, type Element as SlateElement, Path, Transforms} from 'slate'
12
+ import {ReactEditor, useSlateStatic} from 'slate-react'
13
+
14
+ import {debugWithName} from '../../utils/debug'
15
+ import {
16
+ IS_DRAGGING,
17
+ IS_DRAGGING_BLOCK_ELEMENT,
18
+ IS_DRAGGING_BLOCK_TARGET_POSITION,
19
+ IS_DRAGGING_ELEMENT_TARGET,
20
+ } from '../../utils/weakMaps'
21
+
22
+ const debug = debugWithName('components:DraggableBlock')
23
+ const debugRenders = false
24
+
25
+ /**
26
+ * @internal
27
+ */
28
+ export interface DraggableBlockProps {
29
+ children: ReactNode
30
+ element: SlateElement
31
+ readOnly: boolean
32
+ blockRef: MutableRefObject<HTMLDivElement | null>
33
+ }
34
+
35
+ /**
36
+ * Implements drag and drop functionality on editor block nodes
37
+ * @internal
38
+ */
39
+ export const DraggableBlock = ({children, element, readOnly, blockRef}: DraggableBlockProps) => {
40
+ const editor = useSlateStatic()
41
+ const dragGhostRef: MutableRefObject<undefined | HTMLElement> = useRef()
42
+ const [isDragOver, setIsDragOver] = useState(false)
43
+ const isVoid = useMemo(() => Editor.isVoid(editor, element), [editor, element])
44
+ const isInline = useMemo(() => Editor.isInline(editor, element), [editor, element])
45
+
46
+ const [blockElement, setBlockElement] = useState<HTMLElement | null>(null)
47
+
48
+ useEffect(
49
+ () => setBlockElement(blockRef ? blockRef.current : ReactEditor.toDOMNode(editor, element)),
50
+ [editor, element, blockRef],
51
+ )
52
+
53
+ // Note: this is called not for the dragging block, but for the targets when the block is dragged over them
54
+ const handleDragOver = useCallback(
55
+ (event: DragEvent) => {
56
+ const isMyDragOver = IS_DRAGGING_BLOCK_ELEMENT.get(editor)
57
+ // debug('Drag over', blockElement)
58
+ if (!isMyDragOver || !blockElement) {
59
+ return
60
+ }
61
+ event.preventDefault()
62
+ event.dataTransfer.dropEffect = 'move'
63
+ IS_DRAGGING_ELEMENT_TARGET.set(editor, element)
64
+ const elementRect = blockElement.getBoundingClientRect()
65
+ const offset = elementRect.top
66
+ const height = elementRect.height
67
+ const Y = event.pageY
68
+ const loc = Math.abs(offset - Y)
69
+ let position: 'top' | 'bottom' = 'bottom'
70
+ if (element === editor.children[0]) {
71
+ position = 'top'
72
+ } else if (loc < height / 2) {
73
+ position = 'top'
74
+ IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position)
75
+ } else {
76
+ position = 'bottom'
77
+ IS_DRAGGING_BLOCK_TARGET_POSITION.set(editor, position)
78
+ }
79
+ if (isMyDragOver === element) {
80
+ event.dataTransfer.dropEffect = 'none'
81
+ return
82
+ }
83
+ setIsDragOver(true)
84
+ },
85
+ [blockElement, editor, element],
86
+ )
87
+
88
+ // Note: this is called not for the dragging block, but for the targets when the block is dragged over them
89
+ const handleDragLeave = useCallback(() => {
90
+ setIsDragOver(false)
91
+ }, [])
92
+
93
+ // Note: this is called for the dragging block
94
+ const handleDragEnd = useCallback(
95
+ (event: DragEvent) => {
96
+ const targetBlock = IS_DRAGGING_ELEMENT_TARGET.get(editor)
97
+ if (targetBlock) {
98
+ IS_DRAGGING.set(editor, false)
99
+ event.preventDefault()
100
+ event.stopPropagation()
101
+ IS_DRAGGING_ELEMENT_TARGET.delete(editor)
102
+ if (dragGhostRef.current) {
103
+ debug('Removing drag ghost')
104
+ document.body.removeChild(dragGhostRef.current)
105
+ }
106
+ const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor)
107
+ IS_DRAGGING_BLOCK_TARGET_POSITION.delete(editor)
108
+ let targetPath = ReactEditor.findPath(editor, targetBlock)
109
+ const myPath = ReactEditor.findPath(editor, element)
110
+ const isBefore = Path.isBefore(myPath, targetPath)
111
+ if (dragPosition === 'bottom' && !isBefore) {
112
+ // If it is already at the bottom, don't do anything.
113
+ if (targetPath[0] >= editor.children.length - 1) {
114
+ debug('target is already at the bottom, not moving')
115
+ return
116
+ }
117
+ const originalPath = targetPath
118
+ targetPath = Path.next(targetPath)
119
+ debug(
120
+ `Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify(
121
+ targetPath,
122
+ )}`,
123
+ )
124
+ }
125
+ if (dragPosition === 'top' && isBefore && targetPath[0] !== editor.children.length - 1) {
126
+ const originalPath = targetPath
127
+ targetPath = Path.previous(targetPath)
128
+ debug(
129
+ `Adjusting targetPath from ${JSON.stringify(originalPath)} to ${JSON.stringify(
130
+ targetPath,
131
+ )}`,
132
+ )
133
+ }
134
+ if (Path.equals(targetPath, myPath)) {
135
+ event.preventDefault()
136
+ debug('targetPath and myPath is the same, not moving')
137
+ return
138
+ }
139
+ debug(
140
+ `Moving element ${element._key} from path ${JSON.stringify(myPath)} to ${JSON.stringify(
141
+ targetPath,
142
+ )} (${dragPosition})`,
143
+ )
144
+ Transforms.moveNodes(editor, {at: myPath, to: targetPath})
145
+ editor.onChange()
146
+ return
147
+ }
148
+ debug('No target element, not doing anything')
149
+ },
150
+ [editor, element],
151
+ )
152
+ // Note: this is called not for the dragging block, but for the drop target
153
+ const handleDrop = useCallback(
154
+ (event: DragEvent) => {
155
+ if (IS_DRAGGING_BLOCK_ELEMENT.get(editor)) {
156
+ debug('On drop (prevented)', element)
157
+ event.preventDefault()
158
+ event.stopPropagation()
159
+ setIsDragOver(false)
160
+ }
161
+ },
162
+ [editor, element],
163
+ )
164
+ // Note: this is called for the dragging block
165
+ const handleDrag = useCallback(
166
+ (event: DragEvent) => {
167
+ if (!isVoid) {
168
+ IS_DRAGGING_BLOCK_ELEMENT.delete(editor)
169
+ return
170
+ }
171
+ IS_DRAGGING.set(editor, true)
172
+ IS_DRAGGING_BLOCK_ELEMENT.set(editor, element)
173
+ event.stopPropagation() // Stop propagation so that leafs don't get this and take focus/selection!
174
+
175
+ const target = event.target
176
+
177
+ if (target instanceof HTMLElement) {
178
+ target.style.opacity = '1'
179
+ }
180
+ },
181
+ [editor, element, isVoid],
182
+ )
183
+
184
+ // Note: this is called for the dragging block
185
+ const handleDragStart = useCallback(
186
+ (event: DragEvent) => {
187
+ if (!isVoid || isInline) {
188
+ debug('Not dragging block')
189
+ IS_DRAGGING_BLOCK_ELEMENT.delete(editor)
190
+ IS_DRAGGING.set(editor, false)
191
+ return
192
+ }
193
+ debug('Drag start')
194
+ IS_DRAGGING.set(editor, true)
195
+ if (event.dataTransfer) {
196
+ event.dataTransfer.setData('application/portable-text', 'something')
197
+ event.dataTransfer.effectAllowed = 'move'
198
+ }
199
+ // Clone blockElement so that it will not be visually clipped by scroll-containers etc.
200
+ // The application that uses the portable-text-editor may indicate the element used as
201
+ // drag ghost by adding a truthy data attribute 'data-pt-drag-ghost-element' to a HTML element.
202
+ if (blockElement && blockElement instanceof HTMLElement) {
203
+ let dragGhost = blockElement.cloneNode(true) as HTMLElement
204
+ const customGhost = dragGhost.querySelector('[data-pt-drag-ghost-element]')
205
+ if (customGhost) {
206
+ dragGhost = customGhost as HTMLElement
207
+ }
208
+
209
+ // Set the `data-dragged` attribute so the consumer can style the element while it’s dragged
210
+ dragGhost.setAttribute('data-dragged', '')
211
+
212
+ if (document.body) {
213
+ dragGhostRef.current = dragGhost
214
+ dragGhost.style.position = 'absolute'
215
+ dragGhost.style.left = '-99999px'
216
+ dragGhost.style.boxSizing = 'border-box'
217
+ document.body.appendChild(dragGhost)
218
+ const rect = blockElement.getBoundingClientRect()
219
+ const x = event.clientX - rect.left
220
+ const y = event.clientY - rect.top
221
+ dragGhost.style.width = `${rect.width}px`
222
+ dragGhost.style.height = `${rect.height}px`
223
+ event.dataTransfer.setDragImage(dragGhost, x, y)
224
+ }
225
+ }
226
+ handleDrag(event)
227
+ },
228
+ [blockElement, editor, handleDrag, isInline, isVoid],
229
+ )
230
+
231
+ const isDraggingOverFirstBlock =
232
+ isDragOver && editor.children[0] === IS_DRAGGING_ELEMENT_TARGET.get(editor)
233
+ const isDraggingOverLastBlock =
234
+ isDragOver &&
235
+ editor.children[editor.children.length - 1] === IS_DRAGGING_ELEMENT_TARGET.get(editor)
236
+ const dragPosition = IS_DRAGGING_BLOCK_TARGET_POSITION.get(editor)
237
+
238
+ const isDraggingOverTop =
239
+ isDraggingOverFirstBlock ||
240
+ (isDragOver && !isDraggingOverFirstBlock && !isDraggingOverLastBlock && dragPosition === 'top')
241
+ const isDraggingOverBottom =
242
+ isDraggingOverLastBlock ||
243
+ (isDragOver &&
244
+ !isDraggingOverFirstBlock &&
245
+ !isDraggingOverLastBlock &&
246
+ dragPosition === 'bottom')
247
+
248
+ const dropIndicator = useMemo(
249
+ () => (
250
+ <div
251
+ className="pt-drop-indicator"
252
+ style={{
253
+ position: 'absolute',
254
+ width: '100%',
255
+ height: 1,
256
+ borderBottom: '1px solid currentColor',
257
+ zIndex: 5,
258
+ }}
259
+ />
260
+ ),
261
+ [],
262
+ )
263
+
264
+ if (readOnly) {
265
+ return <>{children}</>
266
+ }
267
+
268
+ if (debugRenders) {
269
+ debug('render')
270
+ }
271
+
272
+ return (
273
+ <div
274
+ draggable={isVoid}
275
+ onDragStart={handleDragStart}
276
+ onDrag={handleDrag}
277
+ onDragOver={handleDragOver}
278
+ onDragLeave={handleDragLeave}
279
+ onDragEnd={handleDragEnd}
280
+ onDrop={handleDrop}
281
+ >
282
+ {isDraggingOverTop && dropIndicator}
283
+ {children}
284
+ {isDraggingOverBottom && dropIndicator}
285
+ </div>
286
+ )
287
+ }