@tiptap/extension-drag-handle 2.24.2 → 3.0.0-beta.10

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/LICENSE.md +21 -0
  2. package/dist/index.cjs +463 -457
  3. package/dist/index.cjs.map +1 -1
  4. package/dist/index.d.cts +68 -0
  5. package/dist/index.d.ts +68 -5
  6. package/dist/index.js +441 -450
  7. package/dist/index.js.map +1 -1
  8. package/package.json +20 -18
  9. package/src/drag-handle-plugin.ts +233 -236
  10. package/src/drag-handle.ts +42 -28
  11. package/src/helpers/dragHandler.ts +8 -8
  12. package/src/helpers/findNextElementFromCursor.ts +3 -5
  13. package/src/helpers/getInnerCoords.ts +3 -7
  14. package/src/helpers/getOuterNode.ts +1 -1
  15. package/dist/drag-handle-plugin.d.ts +0 -20
  16. package/dist/drag-handle-plugin.d.ts.map +0 -1
  17. package/dist/drag-handle.d.ts +0 -44
  18. package/dist/drag-handle.d.ts.map +0 -1
  19. package/dist/helpers/cloneElement.d.ts +0 -2
  20. package/dist/helpers/cloneElement.d.ts.map +0 -1
  21. package/dist/helpers/dragHandler.d.ts +0 -3
  22. package/dist/helpers/dragHandler.d.ts.map +0 -1
  23. package/dist/helpers/findNextElementFromCursor.d.ts +0 -14
  24. package/dist/helpers/findNextElementFromCursor.d.ts.map +0 -1
  25. package/dist/helpers/getComputedStyle.d.ts +0 -2
  26. package/dist/helpers/getComputedStyle.d.ts.map +0 -1
  27. package/dist/helpers/getInnerCoords.d.ts +0 -6
  28. package/dist/helpers/getInnerCoords.d.ts.map +0 -1
  29. package/dist/helpers/getOuterNode.d.ts +0 -4
  30. package/dist/helpers/getOuterNode.d.ts.map +0 -1
  31. package/dist/helpers/minMax.d.ts +0 -2
  32. package/dist/helpers/minMax.d.ts.map +0 -1
  33. package/dist/helpers/removeNode.d.ts +0 -2
  34. package/dist/helpers/removeNode.d.ts.map +0 -1
  35. package/dist/index.d.ts.map +0 -1
  36. package/dist/index.umd.js +0 -499
  37. package/dist/index.umd.js.map +0 -1
@@ -1,13 +1,14 @@
1
- import { Editor } from '@tiptap/core'
1
+ import { type ComputePositionConfig, computePosition } from '@floating-ui/dom'
2
+ import type { Editor } from '@tiptap/core'
2
3
  import { isChangeOrigin } from '@tiptap/extension-collaboration'
3
- import { Node } from '@tiptap/pm/model'
4
+ import type { Node } from '@tiptap/pm/model'
5
+ import { type EditorState, type Transaction, Plugin, PluginKey } from '@tiptap/pm/state'
6
+ import type { EditorView } from '@tiptap/pm/view'
4
7
  import {
5
- EditorState, Plugin, PluginKey,
6
- Transaction,
7
- } from '@tiptap/pm/state'
8
- import { EditorView } from '@tiptap/pm/view'
9
- import tippy, { Instance, Props as TippyProps } from 'tippy.js'
10
- import { absolutePositionToRelativePosition, relativePositionToAbsolutePosition, ySyncPluginKey } from 'y-prosemirror'
8
+ absolutePositionToRelativePosition,
9
+ relativePositionToAbsolutePosition,
10
+ ySyncPluginKey,
11
+ } from '@tiptap/y-tiptap'
11
12
 
12
13
  import { dragHandler } from './helpers/dragHandler.js'
13
14
  import { findElementNextToCoords } from './helpers/findNextElementFromCursor.js'
@@ -15,8 +16,8 @@ import { getOuterNode, getOuterNodePos } from './helpers/getOuterNode.js'
15
16
  import { removeNode } from './helpers/removeNode.js'
16
17
 
17
18
  type PluginState = {
18
- locked: boolean;
19
- };
19
+ locked: boolean
20
+ }
20
21
 
21
22
  const getRelativePos = (state: EditorState, absolutePos: number) => {
22
23
  const ystate = ySyncPluginKey.getState(state)
@@ -28,6 +29,7 @@ const getRelativePos = (state: EditorState, absolutePos: number) => {
28
29
  return absolutePositionToRelativePosition(absolutePos, ystate.type, ystate.binding.mapping)
29
30
  }
30
31
 
32
+ // biome-ignore lint/suspicious/noExplicitAny: y-prosemirror (and y-tiptap by extension) does not have types for relative positions
31
33
  const getAbsolutePos = (state: EditorState, relativePos: any) => {
32
34
  const ystate = ySyncPluginKey.getState(state)
33
35
 
@@ -42,7 +44,7 @@ const getOuterDomNode = (view: EditorView, domNode: HTMLElement) => {
42
44
  let tmpDomNode = domNode
43
45
 
44
46
  // Traverse to top level node.
45
- while (tmpDomNode && tmpDomNode.parentNode) {
47
+ while (tmpDomNode?.parentNode) {
46
48
  if (tmpDomNode.parentNode === view.dom) {
47
49
  break
48
50
  }
@@ -54,11 +56,11 @@ const getOuterDomNode = (view: EditorView, domNode: HTMLElement) => {
54
56
  }
55
57
 
56
58
  export interface DragHandlePluginProps {
57
- pluginKey?: PluginKey | string;
58
- editor: Editor;
59
- element: HTMLElement;
60
- onNodeChange?: (data: { editor: Editor; node: Node | null; pos: number }) => void;
61
- tippyOptions?: Partial<TippyProps>;
59
+ pluginKey?: PluginKey | string
60
+ editor: Editor
61
+ element: HTMLElement
62
+ onNodeChange?: (data: { editor: Editor; node: Node | null; pos: number }) => void
63
+ computePositionConfig?: ComputePositionConfig
62
64
  }
63
65
 
64
66
  export const dragHandlePluginDefaultKey = new PluginKey('dragHandle')
@@ -67,17 +69,54 @@ export const DragHandlePlugin = ({
67
69
  pluginKey = dragHandlePluginDefaultKey,
68
70
  element,
69
71
  editor,
70
- tippyOptions,
72
+ computePositionConfig,
71
73
  onNodeChange,
72
74
  }: DragHandlePluginProps) => {
73
75
  const wrapper = document.createElement('div')
74
- let popup: Instance | null = null
75
76
  let locked = false
76
77
  let currentNode: Node | null = null
77
78
  let currentNodePos = -1
79
+ // biome-ignore lint/suspicious/noExplicitAny: See above - relative positions in y-prosemirror are not typed
78
80
  let currentNodeRelPos: any
79
81
 
80
- element.addEventListener('dragstart', e => {
82
+ function hideHandle() {
83
+ if (!element) {
84
+ return
85
+ }
86
+
87
+ element.style.visibility = 'hidden'
88
+ element.style.pointerEvents = 'none'
89
+ }
90
+
91
+ function showHandle() {
92
+ if (!element) {
93
+ return
94
+ }
95
+
96
+ if (!editor.isEditable) {
97
+ hideHandle()
98
+ return
99
+ }
100
+
101
+ element.style.visibility = ''
102
+ element.style.pointerEvents = 'auto'
103
+ }
104
+
105
+ function repositionDragHandle(dom: Element) {
106
+ const virtualElement = {
107
+ getBoundingClientRect: () => dom.getBoundingClientRect(),
108
+ }
109
+
110
+ computePosition(virtualElement, element, computePositionConfig).then(val => {
111
+ Object.assign(element.style, {
112
+ position: val.strategy,
113
+ left: `${val.x}px`,
114
+ top: `${val.y}px`,
115
+ })
116
+ })
117
+ }
118
+
119
+ function onDragStart(e: DragEvent) {
81
120
  // Push this to the end of the event cue
82
121
  // Fixes bug where incorrect drag pos is returned if drag handle has position: absolute
83
122
  // @ts-ignore
@@ -88,283 +127,241 @@ export const DragHandlePlugin = ({
88
127
  element.style.pointerEvents = 'none'
89
128
  }
90
129
  }, 0)
91
- })
130
+ }
92
131
 
93
- element.addEventListener('dragend', () => {
132
+ function onDragEnd() {
133
+ hideHandle()
94
134
  if (element) {
95
135
  element.style.pointerEvents = 'auto'
96
136
  }
97
- })
98
-
99
- return new Plugin({
100
- key: typeof pluginKey === 'string' ? new PluginKey(pluginKey) : pluginKey,
101
-
102
- state: {
103
- init() {
104
- return { locked: false }
105
- },
106
- apply(tr: Transaction, value: PluginState, oldState: EditorState, state: EditorState) {
107
- const isLocked = tr.getMeta('lockDragHandle')
108
- const hideDragHandle = tr.getMeta('hideDragHandle')
109
-
110
- if (isLocked !== undefined) {
111
- locked = isLocked
112
- }
137
+ }
113
138
 
114
- if (hideDragHandle && popup) {
115
- popup.hide()
139
+ element.addEventListener('dragstart', onDragStart)
140
+ element.addEventListener('dragend', onDragEnd)
116
141
 
117
- locked = false
118
- currentNode = null
119
- currentNodePos = -1
142
+ wrapper.appendChild(element)
120
143
 
121
- onNodeChange?.({ editor, node: null, pos: -1 })
144
+ return {
145
+ unbind() {
146
+ element.removeEventListener('dragstart', onDragStart)
147
+ element.removeEventListener('dragend', onDragEnd)
148
+ },
149
+ plugin: new Plugin({
150
+ key: typeof pluginKey === 'string' ? new PluginKey(pluginKey) : pluginKey,
122
151
 
123
- return value
124
- }
152
+ state: {
153
+ init() {
154
+ return { locked: false }
155
+ },
156
+ apply(tr: Transaction, value: PluginState, _oldState: EditorState, state: EditorState) {
157
+ const isLocked = tr.getMeta('lockDragHandle')
158
+ const hideDragHandle = tr.getMeta('hideDragHandle')
125
159
 
126
- // Something has changed and drag handler is visible…
127
- if (tr.docChanged && currentNodePos !== -1 && element && popup) {
128
- // Yjs replaces the entire document on every incoming change and needs a special handling.
129
- // If change comes from another user …
130
- if (isChangeOrigin(tr)) {
131
- // https://discuss.yjs.dev/t/y-prosemirror-mapping-a-single-relative-position-when-doc-changes/851/3
132
- const newPos = getAbsolutePos(state, currentNodeRelPos)
160
+ if (isLocked !== undefined) {
161
+ locked = isLocked
162
+ }
133
163
 
134
- if (newPos !== currentNodePos) {
135
- // Set the new position for our current node.
136
- currentNodePos = newPos
164
+ if (hideDragHandle) {
165
+ hideHandle()
137
166
 
138
- // We will get the outer node with data and position in views update method.
139
- }
140
- } else {
141
- // … otherwise use ProseMirror mapping to update the position.
142
- const newPos = tr.mapping.map(currentNodePos)
167
+ locked = false
168
+ currentNode = null
169
+ currentNodePos = -1
143
170
 
144
- if (newPos !== currentNodePos) {
145
- // TODO: Remove
146
- // console.log('Position has changed …', { old: currentNodePos, new: newPos }, tr);
171
+ onNodeChange?.({ editor, node: null, pos: -1 })
147
172
 
148
- // Set the new position for our current node.
149
- currentNodePos = newPos
173
+ return value
174
+ }
150
175
 
151
- // Memorize relative position to retrieve absolute position in case of collaboration
152
- currentNodeRelPos = getRelativePos(state, currentNodePos)
176
+ // Something has changed and drag handler is visible…
177
+ if (tr.docChanged && currentNodePos !== -1 && element) {
178
+ // Yjs replaces the entire document on every incoming change and needs a special handling.
179
+ // If change comes from another user …
180
+ if (isChangeOrigin(tr)) {
181
+ // https://discuss.yjs.dev/t/y-prosemirror-mapping-a-single-relative-position-when-doc-changes/851/3
182
+ const newPos = getAbsolutePos(state, currentNodeRelPos)
153
183
 
154
- // We will get the outer node with data and position in views update method.
155
- }
156
- }
157
- }
184
+ if (newPos !== currentNodePos) {
185
+ // Set the new position for our current node.
186
+ currentNodePos = newPos
158
187
 
159
- return value
160
- },
161
- },
188
+ // We will get the outer node with data and position in views update method.
189
+ }
190
+ } else {
191
+ // … otherwise use ProseMirror mapping to update the position.
192
+ const newPos = tr.mapping.map(currentNodePos)
162
193
 
163
- view: view => {
164
- element.draggable = true
165
- element.style.pointerEvents = 'auto'
194
+ if (newPos !== currentNodePos) {
195
+ // TODO: Remove
196
+ // console.log('Position has changed …', { old: currentNodePos, new: newPos }, tr);
166
197
 
167
- editor.view.dom.parentElement?.appendChild(wrapper)
198
+ // Set the new position for our current node.
199
+ currentNodePos = newPos
168
200
 
169
- wrapper.appendChild(element)
170
- wrapper.style.pointerEvents = 'none'
171
- wrapper.style.position = 'absolute'
172
- wrapper.style.top = '0'
173
- wrapper.style.left = '0'
201
+ // Memorize relative position to retrieve absolute position in case of collaboration
202
+ currentNodeRelPos = getRelativePos(state, currentNodePos)
174
203
 
175
- return {
176
- update(_, oldState) {
177
- if (!element) {
178
- return
204
+ // We will get the outer node with data and position in views update method.
205
+ }
206
+ }
179
207
  }
180
208
 
181
- if (!editor.isEditable) {
182
- popup?.destroy()
183
- popup = null
184
- return
185
- }
209
+ return value
210
+ },
211
+ },
186
212
 
187
- if (!popup) {
188
- popup = tippy(view.dom, {
189
- getReferenceClientRect: null,
190
- interactive: true,
191
- trigger: 'manual',
192
- placement: 'left-start',
193
- hideOnClick: false,
194
- duration: 100,
195
- popperOptions: {
196
- modifiers: [
197
- { name: 'flip', enabled: false },
198
- {
199
- name: 'preventOverflow',
200
- options: {
201
- rootBoundary: 'document',
202
- mainAxis: false,
203
- },
204
- },
205
- ],
206
- },
207
- ...tippyOptions,
208
- appendTo: wrapper,
209
- content: element,
210
- })
211
- }
213
+ view: view => {
214
+ element.draggable = true
215
+ element.style.pointerEvents = 'auto'
212
216
 
213
- // Prevent element being draggend while being open.
214
- if (locked) {
215
- element.draggable = false
216
- } else {
217
- element.draggable = true
218
- }
217
+ editor.view.dom.parentElement?.appendChild(wrapper)
219
218
 
220
- // Do not close on updates (e.g. changing padding of a section or collaboration events)
221
- // popup?.hide();
219
+ wrapper.style.pointerEvents = 'none'
220
+ wrapper.style.position = 'absolute'
221
+ wrapper.style.top = '0'
222
+ wrapper.style.left = '0'
222
223
 
223
- // Recalculate popup position if doc has changend and drag handler is visible.
224
- if (view.state.doc.eq(oldState.doc) || currentNodePos === -1) {
225
- return
226
- }
224
+ return {
225
+ update(_, oldState) {
226
+ if (!element) {
227
+ return
228
+ }
227
229
 
228
- // Get domNode from (new) position.
229
- let domNode = view.nodeDOM(currentNodePos) as HTMLElement
230
+ if (!editor.isEditable) {
231
+ hideHandle()
232
+ return
233
+ }
230
234
 
231
- // Since old element could have been wrapped, we need to find
232
- // the outer node and take its position and node data.
233
- domNode = getOuterDomNode(view, domNode)
235
+ // Prevent element being draggend while being open.
236
+ if (locked) {
237
+ element.draggable = false
238
+ } else {
239
+ element.draggable = true
240
+ }
234
241
 
235
- // Skip if domNode is editor dom.
236
- if (domNode === view.dom) {
237
- return
238
- }
242
+ // Recalculate popup position if doc has changend and drag handler is visible.
243
+ if (view.state.doc.eq(oldState.doc) || currentNodePos === -1) {
244
+ return
245
+ }
239
246
 
240
- // We only want `Element`.
241
- if (domNode?.nodeType !== 1) {
242
- return
243
- }
247
+ // Get domNode from (new) position.
248
+ let domNode = view.nodeDOM(currentNodePos) as HTMLElement
244
249
 
245
- const domNodePos = view.posAtDOM(domNode, 0)
246
- const outerNode = getOuterNode(editor.state.doc, domNodePos)
247
- const outerNodePos = getOuterNodePos(editor.state.doc, domNodePos) // TODO: needed?
250
+ // Since old element could have been wrapped, we need to find
251
+ // the outer node and take its position and node data.
252
+ domNode = getOuterDomNode(view, domNode)
248
253
 
249
- currentNode = outerNode
250
- currentNodePos = outerNodePos
254
+ // Skip if domNode is editor dom.
255
+ if (domNode === view.dom) {
256
+ return
257
+ }
251
258
 
252
- // Memorize relative position to retrieve absolute position in case of collaboration
253
- currentNodeRelPos = getRelativePos(view.state, currentNodePos)
259
+ // We only want `Element`.
260
+ if (domNode?.nodeType !== 1) {
261
+ return
262
+ }
254
263
 
255
- // TODO: Remove
256
- // console.log('View has updated: callback with new data and repositioning of popup …', {
257
- // domNode,
258
- // currentNodePos,
259
- // currentNode,
260
- // rect: (domNode as Element).getBoundingClientRect(),
261
- // });
264
+ const domNodePos = view.posAtDOM(domNode, 0)
265
+ const outerNode = getOuterNode(editor.state.doc, domNodePos)
266
+ const outerNodePos = getOuterNodePos(editor.state.doc, domNodePos) // TODO: needed?
262
267
 
263
- onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
268
+ currentNode = outerNode
269
+ currentNodePos = outerNodePos
264
270
 
265
- // Update Tippys getReferenceClientRect since domNode might have changed.
266
- popup.setProps({
267
- getReferenceClientRect: () => (domNode as Element).getBoundingClientRect(),
268
- })
269
- },
271
+ // Memorize relative position to retrieve absolute position in case of collaboration
272
+ currentNodeRelPos = getRelativePos(view.state, currentNodePos)
270
273
 
271
- // TODO: Kills even on hot reload
272
- destroy() {
273
- popup?.destroy()
274
+ onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
274
275
 
275
- if (element) {
276
- removeNode(wrapper)
277
- }
278
- },
279
- }
280
- },
276
+ repositionDragHandle(domNode as Element)
277
+ },
281
278
 
282
- props: {
283
- handleDOMEvents: {
284
- mouseleave(_view, e) {
285
- // Do not hide open popup on mouseleave.
286
- if (locked) {
287
- return false
288
- }
279
+ // TODO: Kills even on hot reload
280
+ destroy() {
281
+ if (element) {
282
+ removeNode(wrapper)
283
+ }
284
+ },
285
+ }
286
+ },
289
287
 
290
- // If e.target is not inside the wrapper, hide.
291
- if (e.target && !wrapper.contains(e.relatedTarget as HTMLElement)) {
292
- popup?.hide()
288
+ props: {
289
+ handleDOMEvents: {
290
+ mouseleave(_view, e) {
291
+ // Do not hide open popup on mouseleave.
292
+ if (locked) {
293
+ return false
294
+ }
293
295
 
294
- currentNode = null
295
- currentNodePos = -1
296
+ // If e.target is not inside the wrapper, hide.
297
+ if (e.target && !wrapper.contains(e.relatedTarget as HTMLElement)) {
298
+ hideHandle()
296
299
 
297
- onNodeChange?.({ editor, node: null, pos: -1 })
298
- }
300
+ currentNode = null
301
+ currentNodePos = -1
299
302
 
300
- return false
301
- },
303
+ onNodeChange?.({ editor, node: null, pos: -1 })
304
+ }
302
305
 
303
- mousemove(view, e) {
304
- // Do not continue if popup is not initialized or open.
305
- if (!element || !popup || locked) {
306
306
  return false
307
- }
307
+ },
308
308
 
309
- const nodeData = findElementNextToCoords({
310
- x: e.clientX,
311
- y: e.clientY,
312
- direction: 'right',
313
- editor,
314
- })
309
+ mousemove(view, e) {
310
+ // Do not continue if popup is not initialized or open.
311
+ if (!element || locked) {
312
+ return false
313
+ }
315
314
 
316
- // Skip if there is no node next to coords
317
- if (!nodeData.resultElement) {
318
- return false
319
- }
315
+ const nodeData = findElementNextToCoords({
316
+ x: e.clientX,
317
+ y: e.clientY,
318
+ direction: 'right',
319
+ editor,
320
+ })
320
321
 
321
- let domNode = nodeData.resultElement as HTMLElement
322
+ // Skip if there is no node next to coords
323
+ if (!nodeData.resultElement) {
324
+ return false
325
+ }
322
326
 
323
- domNode = getOuterDomNode(view, domNode)
327
+ let domNode = nodeData.resultElement as HTMLElement
324
328
 
325
- // Skip if domNode is editor dom.
326
- if (domNode === view.dom) {
327
- return false
328
- }
329
+ domNode = getOuterDomNode(view, domNode)
329
330
 
330
- // We only want `Element`.
331
- if (domNode?.nodeType !== 1) {
332
- return false
333
- }
331
+ // Skip if domNode is editor dom.
332
+ if (domNode === view.dom) {
333
+ return false
334
+ }
334
335
 
335
- const domNodePos = view.posAtDOM(domNode, 0)
336
- const outerNode = getOuterNode(editor.state.doc, domNodePos)
336
+ // We only want `Element`.
337
+ if (domNode?.nodeType !== 1) {
338
+ return false
339
+ }
337
340
 
338
- if (outerNode !== currentNode) {
339
- const outerNodePos = getOuterNodePos(editor.state.doc, domNodePos)
341
+ const domNodePos = view.posAtDOM(domNode, 0)
342
+ const outerNode = getOuterNode(editor.state.doc, domNodePos)
340
343
 
341
- currentNode = outerNode
342
- currentNodePos = outerNodePos
344
+ if (outerNode !== currentNode) {
345
+ const outerNodePos = getOuterNodePos(editor.state.doc, domNodePos)
343
346
 
344
- // Memorize relative position to retrieve absolute position in case of collaboration
345
- currentNodeRelPos = getRelativePos(view.state, currentNodePos)
347
+ currentNode = outerNode
348
+ currentNodePos = outerNodePos
346
349
 
347
- // TODO: Remove
348
- // console.log('Mousemove with changed node / node data …', {
349
- // domNode,
350
- // currentNodePos,
351
- // currentNode,
352
- // rect: (domNode as Element).getBoundingClientRect(),
353
- // });
350
+ // Memorize relative position to retrieve absolute position in case of collaboration
351
+ currentNodeRelPos = getRelativePos(view.state, currentNodePos)
354
352
 
355
- onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
353
+ onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
356
354
 
357
- // Set nodes clientRect.
358
- popup.setProps({
359
- getReferenceClientRect: () => (domNode as Element).getBoundingClientRect(),
360
- })
355
+ // Set nodes clientRect.
356
+ repositionDragHandle(domNode as Element)
361
357
 
362
- popup.show()
363
- }
358
+ showHandle()
359
+ }
364
360
 
365
- return false
361
+ return false
362
+ },
366
363
  },
367
364
  },
368
- },
369
- })
365
+ }),
366
+ }
370
367
  }