@tiptap/extension-drag-handle 3.27.1 → 3.27.2
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.
- package/dist/index.cjs +112 -66
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +112 -66
- package/dist/index.js.map +1 -1
- package/package.json +14 -14
- package/src/drag-handle-plugin.ts +59 -39
- package/src/helpers/findNextElementFromCursor.ts +57 -13
- package/src/helpers/nodeRangeDrop.ts +47 -0
|
@@ -14,7 +14,22 @@ export type FindElementNextToCoords = {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* ProseMirror attaches a `pmViewDesc` to every DOM node it manages. Element view
|
|
18
|
+
* descriptions for document nodes expose a `node`; widget decorations (such as
|
|
19
|
+
* the Pages page-chrome overlay, which renders as a zero-height first child of
|
|
20
|
+
* the editor) have a view description but no associated document node.
|
|
21
|
+
*/
|
|
22
|
+
interface ElementWithViewDesc extends Element {
|
|
23
|
+
pmViewDesc?: { node?: unknown }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Finds the draggable block element that is a direct child of view.dom.
|
|
28
|
+
*
|
|
29
|
+
* Direct children that are widget decorations rather than document content are
|
|
30
|
+
* skipped — they are not draggable blocks, and treating them as such would
|
|
31
|
+
* align the drag handle to the decoration (e.g. the page header) instead of the
|
|
32
|
+
* actual first block on the page.
|
|
18
33
|
*/
|
|
19
34
|
export function findClosestTopLevelBlock(
|
|
20
35
|
element: Element,
|
|
@@ -26,7 +41,16 @@ export function findClosestTopLevelBlock(
|
|
|
26
41
|
current = current.parentElement
|
|
27
42
|
}
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
if (current?.parentElement !== view.dom) {
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Skip widget decorations (no associated document node).
|
|
49
|
+
if (!(current as ElementWithViewDesc).pmViewDesc?.node) {
|
|
50
|
+
return undefined
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return current as HTMLElement
|
|
30
54
|
}
|
|
31
55
|
|
|
32
56
|
/**
|
|
@@ -43,6 +67,33 @@ function isValidRect(rect: DOMRect): boolean {
|
|
|
43
67
|
)
|
|
44
68
|
}
|
|
45
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Returns the bounding rect of the first or last child of `container` that has
|
|
72
|
+
* a valid (non-zero) layout box.
|
|
73
|
+
*
|
|
74
|
+
* Some extensions insert zero-size widget decorations as the first/last child
|
|
75
|
+
* of the editor — for example the Pages extension anchors its page-chrome
|
|
76
|
+
* overlay in a zero-height `<div>` as the first child. Reading `firstElementChild`
|
|
77
|
+
* / `lastElementChild` directly would yield an invalid rect and abort clamping,
|
|
78
|
+
* so we skip over any edge children without a valid box.
|
|
79
|
+
*/
|
|
80
|
+
export function edgeBlockRect(container: Element, edge: 'first' | 'last'): DOMRect | null {
|
|
81
|
+
let current: Element | null =
|
|
82
|
+
edge === 'first' ? container.firstElementChild : container.lastElementChild
|
|
83
|
+
|
|
84
|
+
while (current) {
|
|
85
|
+
const rect = current.getBoundingClientRect()
|
|
86
|
+
|
|
87
|
+
if (isValidRect(rect)) {
|
|
88
|
+
return rect
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
current = edge === 'first' ? current.nextElementSibling : current.previousElementSibling
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return null
|
|
95
|
+
}
|
|
96
|
+
|
|
46
97
|
/**
|
|
47
98
|
* Clamps coordinates to content bounds with O(1) layout reads
|
|
48
99
|
*/
|
|
@@ -58,19 +109,12 @@ function clampToContent(
|
|
|
58
109
|
}
|
|
59
110
|
|
|
60
111
|
const container = view.dom
|
|
61
|
-
const firstBlock = container.firstElementChild
|
|
62
|
-
const lastBlock = container.lastElementChild
|
|
63
|
-
|
|
64
|
-
if (!firstBlock || !lastBlock) {
|
|
65
|
-
return null
|
|
66
|
-
}
|
|
67
112
|
|
|
68
|
-
// Clamp Y between first and last
|
|
69
|
-
const topRect =
|
|
70
|
-
const botRect =
|
|
113
|
+
// Clamp Y between the first and last children that actually have a layout box.
|
|
114
|
+
const topRect = edgeBlockRect(container, 'first')
|
|
115
|
+
const botRect = edgeBlockRect(container, 'last')
|
|
71
116
|
|
|
72
|
-
|
|
73
|
-
if (!isValidRect(topRect) || !isValidRect(botRect)) {
|
|
117
|
+
if (!topRect || !botRect) {
|
|
74
118
|
return null
|
|
75
119
|
}
|
|
76
120
|
|
|
@@ -6,6 +6,53 @@ export interface ActiveDragRange {
|
|
|
6
6
|
anchorPos: number
|
|
7
7
|
nodeCount: number
|
|
8
8
|
depth: number
|
|
9
|
+
// Yjs relative position for remapping the drop anchor across isChangeOrigin rebuilds.
|
|
10
|
+
// biome-ignore lint/suspicious/noExplicitAny: y-prosemirror relative positions are untyped
|
|
11
|
+
relativeAnchorPos?: any
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface MapPendingRestoreAnchorOptions {
|
|
15
|
+
isChangeOrigin: boolean
|
|
16
|
+
getAbsolutePos: (relativePos: unknown) => number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Remaps the drop anchor while a restore is pending. Returns null when the anchor
|
|
20
|
+
// can no longer be resolved.
|
|
21
|
+
export function mapPendingRestoreAnchor(
|
|
22
|
+
pendingRestore: ActiveDragRange,
|
|
23
|
+
tr: {
|
|
24
|
+
docChanged: boolean
|
|
25
|
+
mapping: { mapResult: (pos: number, bias: number) => { deleted: boolean; pos: number } }
|
|
26
|
+
},
|
|
27
|
+
options: MapPendingRestoreAnchorOptions,
|
|
28
|
+
): ActiveDragRange | null {
|
|
29
|
+
if (!tr.docChanged) {
|
|
30
|
+
return pendingRestore
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (options.isChangeOrigin && pendingRestore.relativeAnchorPos != null) {
|
|
34
|
+
const newPos = options.getAbsolutePos(pendingRestore.relativeAnchorPos)
|
|
35
|
+
|
|
36
|
+
if (!Number.isFinite(newPos) || newPos <= 0) {
|
|
37
|
+
return null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
...pendingRestore,
|
|
42
|
+
anchorPos: newPos,
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const mappedResult = tr.mapping.mapResult(pendingRestore.anchorPos, 1)
|
|
47
|
+
|
|
48
|
+
if (mappedResult.deleted) {
|
|
49
|
+
return null
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
...pendingRestore,
|
|
54
|
+
anchorPos: mappedResult.pos,
|
|
55
|
+
}
|
|
9
56
|
}
|
|
10
57
|
|
|
11
58
|
interface DroppedBlockRange {
|