@tiptap/extension-drag-handle 2.22.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 (41) hide show
  1. package/README.md +14 -0
  2. package/dist/drag-handle-plugin.d.ts +20 -0
  3. package/dist/drag-handle-plugin.d.ts.map +1 -0
  4. package/dist/drag-handle.d.ts +44 -0
  5. package/dist/drag-handle.d.ts.map +1 -0
  6. package/dist/helpers/cloneElement.d.ts +2 -0
  7. package/dist/helpers/cloneElement.d.ts.map +1 -0
  8. package/dist/helpers/dragHandler.d.ts +3 -0
  9. package/dist/helpers/dragHandler.d.ts.map +1 -0
  10. package/dist/helpers/findNextElementFromCursor.d.ts +14 -0
  11. package/dist/helpers/findNextElementFromCursor.d.ts.map +1 -0
  12. package/dist/helpers/getComputedStyle.d.ts +2 -0
  13. package/dist/helpers/getComputedStyle.d.ts.map +1 -0
  14. package/dist/helpers/getInnerCoords.d.ts +6 -0
  15. package/dist/helpers/getInnerCoords.d.ts.map +1 -0
  16. package/dist/helpers/getOuterNode.d.ts +4 -0
  17. package/dist/helpers/getOuterNode.d.ts.map +1 -0
  18. package/dist/helpers/minMax.d.ts +2 -0
  19. package/dist/helpers/minMax.d.ts.map +1 -0
  20. package/dist/helpers/removeNode.d.ts +2 -0
  21. package/dist/helpers/removeNode.d.ts.map +1 -0
  22. package/dist/index.cjs +504 -0
  23. package/dist/index.cjs.map +1 -0
  24. package/dist/index.d.ts +5 -0
  25. package/dist/index.d.ts.map +1 -0
  26. package/dist/index.js +493 -0
  27. package/dist/index.js.map +1 -0
  28. package/dist/index.umd.js +499 -0
  29. package/dist/index.umd.js.map +1 -0
  30. package/package.json +58 -0
  31. package/src/drag-handle-plugin.ts +370 -0
  32. package/src/drag-handle.ts +92 -0
  33. package/src/helpers/cloneElement.ts +22 -0
  34. package/src/helpers/dragHandler.ts +102 -0
  35. package/src/helpers/findNextElementFromCursor.ts +55 -0
  36. package/src/helpers/getComputedStyle.ts +5 -0
  37. package/src/helpers/getInnerCoords.ts +22 -0
  38. package/src/helpers/getOuterNode.ts +34 -0
  39. package/src/helpers/minMax.ts +3 -0
  40. package/src/helpers/removeNode.ts +3 -0
  41. package/src/index.ts +6 -0
@@ -0,0 +1,370 @@
1
+ import { Editor } from '@tiptap/core'
2
+ import { isChangeOrigin } from '@tiptap/extension-collaboration'
3
+ import { Node } from '@tiptap/pm/model'
4
+ 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'
11
+
12
+ import { dragHandler } from './helpers/dragHandler.js'
13
+ import { findElementNextToCoords } from './helpers/findNextElementFromCursor.js'
14
+ import { getOuterNode, getOuterNodePos } from './helpers/getOuterNode.js'
15
+ import { removeNode } from './helpers/removeNode.js'
16
+
17
+ type PluginState = {
18
+ locked: boolean;
19
+ };
20
+
21
+ const getRelativePos = (state: EditorState, absolutePos: number) => {
22
+ const ystate = ySyncPluginKey.getState(state)
23
+
24
+ if (!ystate) {
25
+ return null
26
+ }
27
+
28
+ return absolutePositionToRelativePosition(absolutePos, ystate.type, ystate.binding.mapping)
29
+ }
30
+
31
+ const getAbsolutePos = (state: EditorState, relativePos: any) => {
32
+ const ystate = ySyncPluginKey.getState(state)
33
+
34
+ if (!ystate) {
35
+ return -1
36
+ }
37
+
38
+ return relativePositionToAbsolutePosition(ystate.doc, ystate.type, relativePos, ystate.binding.mapping) || 0
39
+ }
40
+
41
+ const getOuterDomNode = (view: EditorView, domNode: HTMLElement) => {
42
+ let tmpDomNode = domNode
43
+
44
+ // Traverse to top level node.
45
+ while (tmpDomNode && tmpDomNode.parentNode) {
46
+ if (tmpDomNode.parentNode === view.dom) {
47
+ break
48
+ }
49
+
50
+ tmpDomNode = tmpDomNode.parentNode as HTMLElement
51
+ }
52
+
53
+ return tmpDomNode
54
+ }
55
+
56
+ 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>;
62
+ }
63
+
64
+ export const dragHandlePluginDefaultKey = new PluginKey('dragHandle')
65
+
66
+ export const DragHandlePlugin = ({
67
+ pluginKey = dragHandlePluginDefaultKey,
68
+ element,
69
+ editor,
70
+ tippyOptions,
71
+ onNodeChange,
72
+ }: DragHandlePluginProps) => {
73
+ const wrapper = document.createElement('div')
74
+ let popup: Instance | null = null
75
+ let locked = false
76
+ let currentNode: Node | null = null
77
+ let currentNodePos = -1
78
+ let currentNodeRelPos: any
79
+
80
+ element.addEventListener('dragstart', e => {
81
+ // Push this to the end of the event cue
82
+ // Fixes bug where incorrect drag pos is returned if drag handle has position: absolute
83
+ // @ts-ignore
84
+ dragHandler(e, editor)
85
+
86
+ setTimeout(() => {
87
+ if (element) {
88
+ element.style.pointerEvents = 'none'
89
+ }
90
+ }, 0)
91
+ })
92
+
93
+ element.addEventListener('dragend', () => {
94
+ if (element) {
95
+ element.style.pointerEvents = 'auto'
96
+ }
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
+ }
113
+
114
+ if (hideDragHandle && popup) {
115
+ popup.hide()
116
+
117
+ locked = false
118
+ currentNode = null
119
+ currentNodePos = -1
120
+
121
+ onNodeChange?.({ editor, node: null, pos: -1 })
122
+
123
+ return value
124
+ }
125
+
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)
133
+
134
+ if (newPos !== currentNodePos) {
135
+ // Set the new position for our current node.
136
+ currentNodePos = newPos
137
+
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)
143
+
144
+ if (newPos !== currentNodePos) {
145
+ // TODO: Remove
146
+ // console.log('Position has changed …', { old: currentNodePos, new: newPos }, tr);
147
+
148
+ // Set the new position for our current node.
149
+ currentNodePos = newPos
150
+
151
+ // Memorize relative position to retrieve absolute position in case of collaboration
152
+ currentNodeRelPos = getRelativePos(state, currentNodePos)
153
+
154
+ // We will get the outer node with data and position in views update method.
155
+ }
156
+ }
157
+ }
158
+
159
+ return value
160
+ },
161
+ },
162
+
163
+ view: view => {
164
+ element.draggable = true
165
+ element.style.pointerEvents = 'auto'
166
+
167
+ editor.view.dom.parentElement?.appendChild(wrapper)
168
+
169
+ wrapper.appendChild(element)
170
+ wrapper.style.pointerEvents = 'none'
171
+ wrapper.style.position = 'absolute'
172
+ wrapper.style.top = '0'
173
+ wrapper.style.left = '0'
174
+
175
+ return {
176
+ update(_, oldState) {
177
+ if (!element) {
178
+ return
179
+ }
180
+
181
+ if (!editor.isEditable) {
182
+ popup?.destroy()
183
+ popup = null
184
+ return
185
+ }
186
+
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
+ }
212
+
213
+ // Prevent element being draggend while being open.
214
+ if (locked) {
215
+ element.draggable = false
216
+ } else {
217
+ element.draggable = true
218
+ }
219
+
220
+ // Do not close on updates (e.g. changing padding of a section or collaboration events)
221
+ // popup?.hide();
222
+
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
+ }
227
+
228
+ // Get domNode from (new) position.
229
+ let domNode = view.nodeDOM(currentNodePos) as HTMLElement
230
+
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)
234
+
235
+ // Skip if domNode is editor dom.
236
+ if (domNode === view.dom) {
237
+ return
238
+ }
239
+
240
+ // We only want `Element`.
241
+ if (domNode?.nodeType !== 1) {
242
+ return
243
+ }
244
+
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?
248
+
249
+ currentNode = outerNode
250
+ currentNodePos = outerNodePos
251
+
252
+ // Memorize relative position to retrieve absolute position in case of collaboration
253
+ currentNodeRelPos = getRelativePos(view.state, currentNodePos)
254
+
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
+ // });
262
+
263
+ onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
264
+
265
+ // Update Tippys getReferenceClientRect since domNode might have changed.
266
+ popup.setProps({
267
+ getReferenceClientRect: () => (domNode as Element).getBoundingClientRect(),
268
+ })
269
+ },
270
+
271
+ // TODO: Kills even on hot reload
272
+ destroy() {
273
+ popup?.destroy()
274
+
275
+ if (element) {
276
+ removeNode(wrapper)
277
+ }
278
+ },
279
+ }
280
+ },
281
+
282
+ props: {
283
+ handleDOMEvents: {
284
+ mouseleave(_view, e) {
285
+ // Do not hide open popup on mouseleave.
286
+ if (locked) {
287
+ return false
288
+ }
289
+
290
+ // If e.target is not inside the wrapper, hide.
291
+ if (e.target && !wrapper.contains(e.relatedTarget as HTMLElement)) {
292
+ popup?.hide()
293
+
294
+ currentNode = null
295
+ currentNodePos = -1
296
+
297
+ onNodeChange?.({ editor, node: null, pos: -1 })
298
+ }
299
+
300
+ return false
301
+ },
302
+
303
+ mousemove(view, e) {
304
+ // Do not continue if popup is not initialized or open.
305
+ if (!element || !popup || locked) {
306
+ return false
307
+ }
308
+
309
+ const nodeData = findElementNextToCoords({
310
+ x: e.clientX,
311
+ y: e.clientY,
312
+ direction: 'right',
313
+ editor,
314
+ })
315
+
316
+ // Skip if there is no node next to coords
317
+ if (!nodeData.resultElement) {
318
+ return false
319
+ }
320
+
321
+ let domNode = nodeData.resultElement as HTMLElement
322
+
323
+ domNode = getOuterDomNode(view, domNode)
324
+
325
+ // Skip if domNode is editor dom.
326
+ if (domNode === view.dom) {
327
+ return false
328
+ }
329
+
330
+ // We only want `Element`.
331
+ if (domNode?.nodeType !== 1) {
332
+ return false
333
+ }
334
+
335
+ const domNodePos = view.posAtDOM(domNode, 0)
336
+ const outerNode = getOuterNode(editor.state.doc, domNodePos)
337
+
338
+ if (outerNode !== currentNode) {
339
+ const outerNodePos = getOuterNodePos(editor.state.doc, domNodePos)
340
+
341
+ currentNode = outerNode
342
+ currentNodePos = outerNodePos
343
+
344
+ // Memorize relative position to retrieve absolute position in case of collaboration
345
+ currentNodeRelPos = getRelativePos(view.state, currentNodePos)
346
+
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
+ // });
354
+
355
+ onNodeChange?.({ editor, node: currentNode, pos: currentNodePos })
356
+
357
+ // Set nodes clientRect.
358
+ popup.setProps({
359
+ getReferenceClientRect: () => (domNode as Element).getBoundingClientRect(),
360
+ })
361
+
362
+ popup.show()
363
+ }
364
+
365
+ return false
366
+ },
367
+ },
368
+ },
369
+ })
370
+ }
@@ -0,0 +1,92 @@
1
+ import { Editor, Extension } from '@tiptap/core'
2
+ import { Node } from '@tiptap/pm/model'
3
+ import { Props } from 'tippy.js'
4
+
5
+ import { DragHandlePlugin } from './drag-handle-plugin.js'
6
+
7
+ export interface DragHandleOptions {
8
+ /**
9
+ * Renders an element that is positioned with tippy.js
10
+ */
11
+ render (): HTMLElement,
12
+ /**
13
+ * Options for tippy.js
14
+ */
15
+ tippyOptions?: Partial<Props>,
16
+ /**
17
+ * Locks the draghandle in place and visibility
18
+ */
19
+ locked?: boolean,
20
+ /**
21
+ * Returns a node or null when a node is hovered over
22
+ */
23
+ onNodeChange?: (options: { node: Node | null, editor: Editor }) => void,
24
+ }
25
+
26
+ declare module '@tiptap/core' {
27
+ interface Commands<ReturnType> {
28
+ dragHandle: {
29
+ /**
30
+ * Locks the draghandle in place and visibility
31
+ */
32
+ lockDragHandle: () => ReturnType,
33
+ /**
34
+ * Unlocks the draghandle
35
+ */
36
+ unlockDragHandle: () => ReturnType,
37
+ /**
38
+ * Toggle draghandle lock state
39
+ */
40
+ toggleDragHandle: () => ReturnType,
41
+ }
42
+ }
43
+ }
44
+
45
+ export const DragHandle = Extension.create<DragHandleOptions>({
46
+ name: 'dragHandle',
47
+
48
+ addOptions() {
49
+ return {
50
+ render() {
51
+ const element = document.createElement('div')
52
+
53
+ element.classList.add('drag-handle')
54
+
55
+ return element
56
+ },
57
+ tippyOptions: {},
58
+ locked: false,
59
+ onNodeChange: () => { return null },
60
+ }
61
+ },
62
+
63
+ addCommands() {
64
+ return {
65
+ lockDragHandle: () => ({ editor }) => {
66
+ this.options.locked = true
67
+ return editor.commands.setMeta('lockDragHandle', this.options.locked)
68
+ },
69
+ unlockDragHandle: () => ({ editor }) => {
70
+ this.options.locked = false
71
+ return editor.commands.setMeta('lockDragHandle', this.options.locked)
72
+ },
73
+ toggleDragHandle: () => ({ editor }) => {
74
+ this.options.locked = !this.options.locked
75
+ return editor.commands.setMeta('lockDragHandle', this.options.locked)
76
+ },
77
+ }
78
+ },
79
+
80
+ addProseMirrorPlugins() {
81
+ const element = this.options.render()
82
+
83
+ return [
84
+ DragHandlePlugin({
85
+ tippyOptions: this.options.tippyOptions,
86
+ element,
87
+ editor: this.editor,
88
+ onNodeChange: this.options.onNodeChange,
89
+ }),
90
+ ]
91
+ },
92
+ })
@@ -0,0 +1,22 @@
1
+ function getCSSText(element: Element) {
2
+ let value = ''
3
+ const style = getComputedStyle(element)
4
+
5
+ for (let i = 0; i < style.length; i += 1) {
6
+ value += `${style[i]}:${style.getPropertyValue(style[i])};`
7
+ }
8
+
9
+ return value
10
+ }
11
+
12
+ export function cloneElement(node: HTMLElement) {
13
+ const clonedNode = node.cloneNode(true) as HTMLElement
14
+ const sourceElements = [node, ...Array.from(node.getElementsByTagName('*'))] as HTMLElement[]
15
+ const targetElements = [clonedNode, ...Array.from(clonedNode.getElementsByTagName('*'))] as HTMLElement[]
16
+
17
+ sourceElements.forEach((sourceElement, index) => {
18
+ targetElements[index].style.cssText = getCSSText(sourceElement)
19
+ })
20
+
21
+ return clonedNode
22
+ }
@@ -0,0 +1,102 @@
1
+ import { Editor } from '@tiptap/core'
2
+ import { getSelectionRanges, NodeRangeSelection } from '@tiptap/extension-node-range'
3
+ import { SelectionRange } from '@tiptap/pm/state'
4
+
5
+ import { cloneElement } from './cloneElement.js'
6
+ import { findElementNextToCoords } from './findNextElementFromCursor.js'
7
+ import { getInnerCoords } from './getInnerCoords.js'
8
+ import { removeNode } from './removeNode.js'
9
+
10
+ function getDragHandleRanges(event: DragEvent, editor: Editor): SelectionRange[] {
11
+ const { doc } = editor.view.state
12
+
13
+ const result = findElementNextToCoords({
14
+ editor, x: event.clientX, y: event.clientY, direction: 'right',
15
+ })
16
+
17
+ if (!result.resultNode || result.pos === null) {
18
+ return []
19
+ }
20
+
21
+ const x = event.clientX
22
+
23
+ // @ts-ignore
24
+ const coords = getInnerCoords(editor.view, x, event.clientY)
25
+ const posAtCoords = editor.view.posAtCoords(coords)
26
+
27
+ if (!posAtCoords) {
28
+ return []
29
+ }
30
+
31
+ const { pos } = posAtCoords
32
+ const nodeAt = doc.resolve(pos).parent
33
+
34
+ if (!nodeAt) {
35
+ return []
36
+ }
37
+
38
+ const $from = doc.resolve(result.pos)
39
+ const $to = doc.resolve(result.pos + 1)
40
+
41
+ return getSelectionRanges($from, $to, 0)
42
+ }
43
+
44
+ export function dragHandler(event: DragEvent, editor: Editor) {
45
+ const { view } = editor
46
+
47
+ if (!event.dataTransfer) {
48
+ return
49
+ }
50
+
51
+ const { empty, $from, $to } = view.state.selection
52
+
53
+ const dragHandleRanges = getDragHandleRanges(event, editor)
54
+
55
+ const selectionRanges = getSelectionRanges($from, $to, 0)
56
+ const isDragHandleWithinSelection = selectionRanges.some(range => {
57
+ return dragHandleRanges.find(dragHandleRange => {
58
+ return dragHandleRange.$from === range.$from
59
+ && dragHandleRange.$to === range.$to
60
+ })
61
+ })
62
+
63
+ const ranges = empty || !isDragHandleWithinSelection
64
+ ? dragHandleRanges
65
+ : selectionRanges
66
+
67
+ if (!ranges.length) {
68
+ return
69
+ }
70
+
71
+ const { tr } = view.state
72
+ const wrapper = document.createElement('div')
73
+ const from = ranges[0].$from.pos
74
+ const to = ranges[ranges.length - 1].$to.pos
75
+
76
+ const selection = NodeRangeSelection.create(view.state.doc, from, to)
77
+ const slice = selection.content()
78
+
79
+ ranges.forEach(range => {
80
+ const element = view.nodeDOM(range.$from.pos) as HTMLElement
81
+ const clonedElement = cloneElement(element)
82
+
83
+ wrapper.append(clonedElement)
84
+ })
85
+
86
+ wrapper.style.position = 'absolute'
87
+ wrapper.style.top = '-10000px'
88
+ document.body.append(wrapper)
89
+
90
+ event.dataTransfer.clearData()
91
+ event.dataTransfer.setDragImage(wrapper, 0, 0)
92
+
93
+ // tell ProseMirror the dragged content
94
+ view.dragging = { slice, move: true }
95
+
96
+ tr.setSelection(selection)
97
+
98
+ view.dispatch(tr)
99
+
100
+ // clean up
101
+ document.addEventListener('drop', () => removeNode(wrapper), { once: true })
102
+ }
@@ -0,0 +1,55 @@
1
+ import { Editor } from '@tiptap/core'
2
+ import { Node } from '@tiptap/pm/model'
3
+
4
+ export type FindElementNextToCoords = {
5
+ x: number
6
+ y: number
7
+ direction?: 'left' | 'right'
8
+ editor: Editor
9
+ }
10
+
11
+ export const findElementNextToCoords = (options: FindElementNextToCoords) => {
12
+ const {
13
+ x, y, direction, editor,
14
+ } = options
15
+ let resultElement: HTMLElement | null = null
16
+ let resultNode: Node | null = null
17
+ let pos: number | null = null
18
+
19
+ let currentX = x
20
+
21
+ while (resultNode === null && currentX < window.innerWidth && currentX > 0) {
22
+ const allElements = document.elementsFromPoint(currentX, y)
23
+ const prosemirrorIndex = allElements.findIndex(element => element.classList.contains('ProseMirror'))
24
+ const filteredElements = allElements.slice(0, prosemirrorIndex)
25
+
26
+ if (filteredElements.length > 0) {
27
+ const target = filteredElements[0]
28
+
29
+ resultElement = target as HTMLElement
30
+ pos = editor.view.posAtDOM(target, 0)
31
+
32
+ if (pos >= 0) {
33
+ resultNode = editor.state.doc.nodeAt(Math.max(pos - 1, 0))
34
+
35
+ if (resultNode?.isText) {
36
+ resultNode = editor.state.doc.nodeAt(Math.max(pos - 1, 0))
37
+ }
38
+
39
+ if (!resultNode) {
40
+ resultNode = editor.state.doc.nodeAt(Math.max(pos, 0))
41
+ }
42
+
43
+ break
44
+ }
45
+ }
46
+
47
+ if (direction === 'left') {
48
+ currentX -= 1
49
+ } else {
50
+ currentX += 1
51
+ }
52
+ }
53
+
54
+ return { resultElement, resultNode, pos: pos ?? null }
55
+ }
@@ -0,0 +1,5 @@
1
+ export function getComputedStyle(node: Element, property: keyof CSSStyleDeclaration): any {
2
+ const style = window.getComputedStyle(node)
3
+
4
+ return style[property]
5
+ }
@@ -0,0 +1,22 @@
1
+ import { EditorView } from '@tiptap/pm/view'
2
+
3
+ import { getComputedStyle } from './getComputedStyle.js'
4
+ import { minMax } from './minMax.js'
5
+
6
+ export function getInnerCoords(view: EditorView, x: number, y: number): { left: number, top: number } {
7
+ const paddingLeft = parseInt(getComputedStyle(view.dom, 'paddingLeft'), 10)
8
+ const paddingRight = parseInt(getComputedStyle(view.dom, 'paddingRight'), 10)
9
+ const borderLeft = parseInt(getComputedStyle(view.dom, 'borderLeftWidth'), 10)
10
+ const borderRight = parseInt(getComputedStyle(view.dom, 'borderLeftWidth'), 10)
11
+ const bounds = view.dom.getBoundingClientRect()
12
+ const coords = {
13
+ left: minMax(
14
+ x,
15
+ bounds.left + paddingLeft + borderLeft,
16
+ bounds.right - paddingRight - borderRight,
17
+ ),
18
+ top: y,
19
+ }
20
+
21
+ return coords
22
+ }
@@ -0,0 +1,34 @@
1
+ import { Node } from '@tiptap/pm/model'
2
+
3
+ export const getOuterNodePos = (doc: Node, pos: number): number => {
4
+ const resolvedPos = doc.resolve(pos)
5
+ const { depth } = resolvedPos
6
+
7
+ if (depth === 0) {
8
+ return pos
9
+ }
10
+
11
+ const a = resolvedPos.pos - resolvedPos.parentOffset
12
+
13
+ return a - 1
14
+ }
15
+
16
+ export const getOuterNode = (doc: Node, pos: number): Node | null => {
17
+ const node = doc.nodeAt(pos)
18
+ const resolvedPos = doc.resolve(pos)
19
+
20
+ let { depth } = resolvedPos
21
+ let parent = node
22
+
23
+ while (depth > 0) {
24
+ const currentNode = resolvedPos.node(depth)
25
+
26
+ depth -= 1
27
+
28
+ if (depth === 0) {
29
+ parent = currentNode
30
+ }
31
+ }
32
+
33
+ return parent
34
+ }