@nexxtmove/ui 1.37.0 → 1.38.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.
- package/dist/components/molecules/Tree/Tree.vue.d.ts +25 -0
- package/dist/components/molecules/Tree/tree-drop.d.ts +81 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +786 -508
- package/package.json +1 -1
- package/src/components/molecules/Tree/Tree.vue +573 -72
- package/src/components/molecules/Tree/tree-drop.ts +155 -0
- package/src/index.ts +1 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import type { NexxtTreeNode } from './Tree.vue'
|
|
2
|
+
|
|
3
|
+
/** A visible row of the tree, in document order. */
|
|
4
|
+
export interface TreeRow {
|
|
5
|
+
node: NexxtTreeNode
|
|
6
|
+
depth: number
|
|
7
|
+
parentId: string | null
|
|
8
|
+
expandable: boolean
|
|
9
|
+
canHaveChildren: boolean
|
|
10
|
+
draggable: boolean
|
|
11
|
+
expanded: boolean
|
|
12
|
+
/** One-based position among its siblings. */
|
|
13
|
+
position: number
|
|
14
|
+
setSize: number
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type DropMarker = 'before' | 'inside' | 'after'
|
|
18
|
+
|
|
19
|
+
export interface TreeDrop {
|
|
20
|
+
/** The row the pointer was over when this spot was picked. */
|
|
21
|
+
rowId: string
|
|
22
|
+
parentId: string | null
|
|
23
|
+
index: number
|
|
24
|
+
marker: DropMarker
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Where a row sits with the ghost row taken out, relative to the top of the tree. `bottom` takes
|
|
29
|
+
* in the gap below the row: a gap belongs to the row above it, so moving across it never reads
|
|
30
|
+
* as leaving the tree.
|
|
31
|
+
*/
|
|
32
|
+
export interface RestSlot {
|
|
33
|
+
id: string
|
|
34
|
+
top: number
|
|
35
|
+
height: number
|
|
36
|
+
bottom: number
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DropLayout {
|
|
40
|
+
slots: RestSlot[]
|
|
41
|
+
/** The ghost row as it is laid out now, gap included; `null` while there is none. */
|
|
42
|
+
ghost: { top: number; bottom: number } | null
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** `keep` leaves the current spot as it is: the pointer is on the ghost row, or there are no rows. */
|
|
46
|
+
export type DropHit = { kind: 'keep' } | { kind: 'row'; id: string; ratio: number }
|
|
47
|
+
|
|
48
|
+
export const containsNode = (node: NexxtTreeNode, id: string): boolean =>
|
|
49
|
+
node.id === id || !!node.children?.some((child) => containsNode(child, id))
|
|
50
|
+
|
|
51
|
+
export const findNode = (nodes: NexxtTreeNode[], id: string): NexxtTreeNode | null => {
|
|
52
|
+
for (const node of nodes) {
|
|
53
|
+
if (node.id === id) return node
|
|
54
|
+
|
|
55
|
+
const found = node.children ? findNode(node.children, id) : null
|
|
56
|
+
if (found) return found
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Reads the pointer against where the rows land without the ghost row, not against what is under
|
|
64
|
+
* it now. The ghost pushes rows aside, so reading what is under the pointer would find a shifted
|
|
65
|
+
* row or the ghost itself, pick another spot, move the ghost and flicker. Measured this way the
|
|
66
|
+
* same pointer always gives the same answer, however the ghost moves.
|
|
67
|
+
*
|
|
68
|
+
* @param layout - The rows at rest and the ghost row as it is laid out now.
|
|
69
|
+
* @param y - The pointer, relative to the top of the tree.
|
|
70
|
+
*/
|
|
71
|
+
export const hitTest = ({ slots, ghost }: DropLayout, y: number): DropHit => {
|
|
72
|
+
if (ghost && y >= ghost.top && y < ghost.bottom) return { kind: 'keep' }
|
|
73
|
+
|
|
74
|
+
const restY = ghost && y >= ghost.bottom ? y - (ghost.bottom - ghost.top) : y
|
|
75
|
+
const last = slots.at(-1)
|
|
76
|
+
|
|
77
|
+
// The empty space a tree can have below its last row is the end of that row's level, not a gap
|
|
78
|
+
if (last && restY >= last.bottom) return { kind: 'row', id: last.id, ratio: 1 }
|
|
79
|
+
|
|
80
|
+
const slot = slots.find((item) => restY >= item.top && restY < item.bottom)
|
|
81
|
+
if (!slot) return { kind: 'keep' }
|
|
82
|
+
|
|
83
|
+
const ratio = slot.height ? (restY - slot.top) / slot.height : 0
|
|
84
|
+
|
|
85
|
+
return { kind: 'row', id: slot.id, ratio: Math.min(1, Math.max(0, ratio)) }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The spot a pointer at `ratio` of `row` stands for. A group splits into before (top 30%),
|
|
90
|
+
* inside and after (bottom 30%), a leaf into before and after at the middle. Inside always lands
|
|
91
|
+
* as the first child, right under the group row, so the ghost shows up next to the pointer. The
|
|
92
|
+
* dragged node and everything inside it are no target at all.
|
|
93
|
+
*/
|
|
94
|
+
export const resolveDrop = (
|
|
95
|
+
row: TreeRow,
|
|
96
|
+
ratio: number,
|
|
97
|
+
moving: NexxtTreeNode,
|
|
98
|
+
): TreeDrop | null => {
|
|
99
|
+
if (containsNode(moving, row.node.id)) return null
|
|
100
|
+
|
|
101
|
+
if (row.canHaveChildren && ratio > 0.3 && ratio < 0.7) {
|
|
102
|
+
return { rowId: row.node.id, parentId: row.node.id, index: 0, marker: 'inside' }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const after = ratio >= 0.5
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
rowId: row.node.id,
|
|
109
|
+
parentId: row.parentId,
|
|
110
|
+
index: row.position - 1 + (after ? 1 : 0),
|
|
111
|
+
marker: after ? 'after' : 'before',
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** A spot where the dragged row already is: dropping there changes nothing, so it is no target. */
|
|
116
|
+
export const isNoOp = (drop: TreeDrop, source: TreeRow) =>
|
|
117
|
+
drop.parentId === source.parentId &&
|
|
118
|
+
(drop.index === source.position - 1 || drop.index === source.position)
|
|
119
|
+
|
|
120
|
+
/** Two spots are the same when they land the node in the same place, whichever row picked them. */
|
|
121
|
+
export const samePlace = (a: TreeDrop | null, b: TreeDrop | null) =>
|
|
122
|
+
a === b || (!!a && !!b && a.parentId === b.parentId && a.index === b.index)
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Where the ghost row goes among the visible rows, and how deep. Right before the row of the
|
|
126
|
+
* sibling now at `index`, or after the last visible row inside the parent when `index` is past
|
|
127
|
+
* its last child. `null` for a spot inside a collapsed group: that group row marks itself.
|
|
128
|
+
*/
|
|
129
|
+
export const ghostSlot = (
|
|
130
|
+
rows: TreeRow[],
|
|
131
|
+
nodes: NexxtTreeNode[],
|
|
132
|
+
drop: TreeDrop | null,
|
|
133
|
+
): { at: number; depth: number } | null => {
|
|
134
|
+
if (!drop) return null
|
|
135
|
+
|
|
136
|
+
const parentAt =
|
|
137
|
+
drop.parentId === null ? -1 : rows.findIndex((row) => row.node.id === drop.parentId)
|
|
138
|
+
const parent = rows[parentAt]
|
|
139
|
+
if (drop.parentId !== null && !parent) return null
|
|
140
|
+
if (parent?.expandable && !parent.expanded) return null
|
|
141
|
+
|
|
142
|
+
const depth = parent ? parent.depth + 1 : 0
|
|
143
|
+
const next = (parent ? (parent.node.children ?? []) : nodes)[drop.index]
|
|
144
|
+
|
|
145
|
+
if (next) {
|
|
146
|
+
const at = rows.findIndex((row) => row.node.id === next.id)
|
|
147
|
+
|
|
148
|
+
return at === -1 ? null : { at, depth }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let at = parentAt + 1
|
|
152
|
+
while (at < rows.length && rows[at]!.depth >= depth) at++
|
|
153
|
+
|
|
154
|
+
return { at, depth }
|
|
155
|
+
}
|