@splicetree/plugin-dnd 3.0.2 → 3.1.1
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/CHANGELOG.md +12 -0
- package/dist/index.d.ts +25 -10
- package/dist/index.js +170 -38
- package/package.json +2 -2
- package/src/index.ts +213 -39
- package/src/types.ts +24 -5
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @splicetree/plugin-dnd
|
|
2
2
|
|
|
3
|
+
## 3.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Core/Adapter/Lazy-Load/DnD:稳定性修复与小幅改进
|
|
8
|
+
- DnD:为 `draggable/sortable/nestable` 增加函数/异步判定;在悬停与释放阶段严格校验并在不允许时隐藏 ghost;增加去抖与离开清理以消除偶发 ghost 闪烁
|
|
9
|
+
- Adapter-Vue:抽取数据同步工具;字段变更走 `updateOriginal` 合并,结构变更再 `syncData`
|
|
10
|
+
- Core:节点扩展应用于追加节点;补充若干示例与文档更新
|
|
11
|
+
- Lazy-Load:覆盖 `syncData`,在重建后恢复已加载子树与覆盖
|
|
12
|
+
|
|
13
|
+
## 3.1.0
|
|
14
|
+
|
|
3
15
|
## 3.0.2
|
|
4
16
|
|
|
5
17
|
## 3.0.1
|
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
|
-
import { SpliceTreePlugin } from "@splicetree/core";
|
|
1
|
+
import { SpliceTreeNode as SpliceTreeNode$1, SpliceTreePlugin } from "@splicetree/core";
|
|
2
2
|
|
|
3
3
|
//#region src/types.d.ts
|
|
4
|
-
/** dnd types */
|
|
5
4
|
interface DndNode {
|
|
6
5
|
id: string;
|
|
7
6
|
level: number;
|
|
8
7
|
parentId?: string;
|
|
9
8
|
root?: boolean;
|
|
10
9
|
}
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
type BehaviorPredicate<T = unknown> = boolean | ((target: SpliceTreeNode$1<T>) => boolean | Promise<boolean>);
|
|
11
|
+
interface DragBehavior<T = unknown> {
|
|
12
|
+
/**
|
|
13
|
+
* 控制是否允许作为拖拽源在当前目标上执行拖放
|
|
14
|
+
* - 布尔:true/false 全局允许或禁止
|
|
15
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
16
|
+
*/
|
|
17
|
+
draggable?: BehaviorPredicate<T>;
|
|
18
|
+
/**
|
|
19
|
+
* 控制是否允许在目标节点的前后排序
|
|
20
|
+
* - 布尔:true/false 全局允许或禁止
|
|
21
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
22
|
+
*/
|
|
23
|
+
sortable?: BehaviorPredicate<T>;
|
|
24
|
+
/**
|
|
25
|
+
* 控制能否作为子节点拖入
|
|
26
|
+
* - 布尔:true/false 全局允许或禁止
|
|
27
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
28
|
+
*/
|
|
29
|
+
nestable?: BehaviorPredicate<T>;
|
|
15
30
|
}
|
|
16
31
|
declare enum DropPosition {
|
|
17
32
|
BEFORE = -1,
|
|
@@ -36,13 +51,13 @@ declare module '@splicetree/core' {
|
|
|
36
51
|
interface SpliceTreeConfiguration {
|
|
37
52
|
dnd?: DndOptions;
|
|
38
53
|
}
|
|
39
|
-
interface SpliceTreeEventPayloadMap {
|
|
54
|
+
interface SpliceTreeEventPayloadMap<T = unknown> {
|
|
40
55
|
move: {
|
|
41
56
|
index: number;
|
|
42
|
-
parent: SpliceTreeNode<
|
|
57
|
+
parent: SpliceTreeNode<T> | undefined;
|
|
43
58
|
oldIndex: number;
|
|
44
|
-
oldParent: SpliceTreeNode<
|
|
45
|
-
node: SpliceTreeNode<
|
|
59
|
+
oldParent: SpliceTreeNode<T> | undefined;
|
|
60
|
+
node: SpliceTreeNode<T>;
|
|
46
61
|
event?: DragEvent | MouseEvent;
|
|
47
62
|
};
|
|
48
63
|
}
|
package/dist/index.js
CHANGED
|
@@ -43,6 +43,8 @@ const dnd = {
|
|
|
43
43
|
let containerHoverParentId;
|
|
44
44
|
let containerHoverPos;
|
|
45
45
|
let lastHoverTargetId;
|
|
46
|
+
let evalToken = 0;
|
|
47
|
+
let evaluating = false;
|
|
46
48
|
const refresh = () => ctx.events.emit({
|
|
47
49
|
name: "visibility",
|
|
48
50
|
keys: ctx.tree.expandedKeys()
|
|
@@ -109,12 +111,21 @@ const dnd = {
|
|
|
109
111
|
* @param el 目标节点元素
|
|
110
112
|
* @param e 拖拽/鼠标事件
|
|
111
113
|
*/
|
|
112
|
-
const onDragOver = (id, el, e) => {
|
|
114
|
+
const onDragOver = async (id, el, e) => {
|
|
113
115
|
if (!draggingId) return;
|
|
116
|
+
evalToken++;
|
|
117
|
+
const myToken = evalToken;
|
|
118
|
+
evaluating = true;
|
|
119
|
+
ghostPos = void 0;
|
|
120
|
+
ctx.events.emit({
|
|
121
|
+
name: "visibility",
|
|
122
|
+
keys: ctx.tree.expandedKeys()
|
|
123
|
+
});
|
|
114
124
|
const draggedIds = getDraggedNodeIds(draggingId);
|
|
115
125
|
const targetNode = ctx.tree.getNode(id);
|
|
116
126
|
if (draggedIds.includes(id)) {
|
|
117
127
|
hoverPositions.delete(id);
|
|
128
|
+
ghostPos = void 0;
|
|
118
129
|
ctx.events.emit({
|
|
119
130
|
name: "visibility",
|
|
120
131
|
keys: ctx.tree.expandedKeys()
|
|
@@ -125,6 +136,7 @@ const dnd = {
|
|
|
125
136
|
while (ancestor) {
|
|
126
137
|
if (draggedIds.includes(ancestor.id)) {
|
|
127
138
|
hoverPositions.delete(id);
|
|
139
|
+
ghostPos = void 0;
|
|
128
140
|
ctx.events.emit({
|
|
129
141
|
name: "visibility",
|
|
130
142
|
keys: ctx.tree.expandedKeys()
|
|
@@ -136,10 +148,12 @@ const dnd = {
|
|
|
136
148
|
const pos = computePosition(id, el, e, void 0);
|
|
137
149
|
if (!targetNode || pos === void 0) {
|
|
138
150
|
hoverPositions.delete(id);
|
|
151
|
+
ghostPos = void 0;
|
|
139
152
|
ctx.events.emit({
|
|
140
153
|
name: "visibility",
|
|
141
154
|
keys: ctx.tree.expandedKeys()
|
|
142
155
|
});
|
|
156
|
+
evaluating = false;
|
|
143
157
|
return;
|
|
144
158
|
}
|
|
145
159
|
if (pos === DropPosition.INSIDE) {
|
|
@@ -153,24 +167,83 @@ const dnd = {
|
|
|
153
167
|
});
|
|
154
168
|
return;
|
|
155
169
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
170
|
+
let allNestable = true;
|
|
171
|
+
for (const dId of draggedIds) {
|
|
172
|
+
const rule = behaviors.get(dId)?.nestable;
|
|
173
|
+
if (rule === false) {
|
|
174
|
+
allNestable = false;
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
if (typeof rule === "function") try {
|
|
178
|
+
const ret = rule(targetNode);
|
|
179
|
+
if (!(ret instanceof Promise ? await ret : ret)) {
|
|
180
|
+
allNestable = false;
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
allNestable = false;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (!allNestable) {
|
|
159
189
|
hoverPositions.delete(id);
|
|
190
|
+
ghostPos = void 0;
|
|
160
191
|
ctx.events.emit({
|
|
161
192
|
name: "visibility",
|
|
162
193
|
keys: ctx.tree.expandedKeys()
|
|
163
194
|
});
|
|
195
|
+
evaluating = false;
|
|
164
196
|
return;
|
|
165
197
|
}
|
|
166
|
-
} else
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
198
|
+
} else {
|
|
199
|
+
let allSortable = true;
|
|
200
|
+
for (const dId of draggedIds) {
|
|
201
|
+
const ov = behaviors.get(dId);
|
|
202
|
+
const sRule = ov?.sortable;
|
|
203
|
+
if (typeof sRule === "boolean" && sRule === false) {
|
|
204
|
+
allSortable = false;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
if (typeof sRule === "function") try {
|
|
208
|
+
const ret = sRule(targetNode);
|
|
209
|
+
if (!(ret instanceof Promise ? await ret : ret)) {
|
|
210
|
+
allSortable = false;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
} catch {
|
|
214
|
+
allSortable = false;
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const dRuleRaw = ov?.draggable;
|
|
218
|
+
if (typeof dRuleRaw !== "function") {
|
|
219
|
+
if (dRuleRaw === false) {
|
|
220
|
+
allSortable = false;
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
223
|
+
} else try {
|
|
224
|
+
const ret = dRuleRaw(targetNode);
|
|
225
|
+
if (!(ret instanceof Promise ? await ret : ret)) {
|
|
226
|
+
allSortable = false;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
} catch {
|
|
230
|
+
allSortable = false;
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (!allSortable) {
|
|
235
|
+
hoverPositions.delete(id);
|
|
236
|
+
ghostPos = void 0;
|
|
237
|
+
ctx.events.emit({
|
|
238
|
+
name: "visibility",
|
|
239
|
+
keys: ctx.tree.expandedKeys()
|
|
240
|
+
});
|
|
241
|
+
evaluating = false;
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (myToken !== evalToken || !draggingId) {
|
|
246
|
+
evaluating = false;
|
|
174
247
|
return;
|
|
175
248
|
}
|
|
176
249
|
hoverPositions.set(id, pos);
|
|
@@ -189,34 +262,18 @@ const dnd = {
|
|
|
189
262
|
ghostMarginLeft = 0;
|
|
190
263
|
ghostMarginRight = 0;
|
|
191
264
|
}
|
|
265
|
+
evaluating = false;
|
|
192
266
|
refresh();
|
|
193
267
|
};
|
|
194
268
|
/**
|
|
195
269
|
* 离开目标:清理该目标的悬停位置并刷新视图
|
|
196
270
|
* @param id 目标节点 id
|
|
197
271
|
*/
|
|
198
|
-
const onDragLeave = (id,
|
|
272
|
+
const onDragLeave = (id, _e) => {
|
|
199
273
|
if (!draggingId) return;
|
|
200
|
-
|
|
201
|
-
if (
|
|
202
|
-
|
|
203
|
-
name: "visibility",
|
|
204
|
-
keys: ctx.tree.expandedKeys()
|
|
205
|
-
});
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
const rect = el.getBoundingClientRect();
|
|
209
|
-
const cy = e?.clientY;
|
|
210
|
-
if (typeof cy === "number") {
|
|
211
|
-
if (cy < rect.top) {
|
|
212
|
-
hoverPositions.set(id, DropPosition.BEFORE);
|
|
213
|
-
setGhostByEl(el, DropPosition.BEFORE);
|
|
214
|
-
} else if (cy > rect.bottom) {
|
|
215
|
-
hoverPositions.set(id, DropPosition.AFTER);
|
|
216
|
-
setGhostByEl(el, DropPosition.AFTER);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
lastHoverTargetId = id;
|
|
274
|
+
hoverPositions.delete(id);
|
|
275
|
+
if (lastHoverTargetId === id) lastHoverTargetId = void 0;
|
|
276
|
+
ghostPos = void 0;
|
|
220
277
|
refresh();
|
|
221
278
|
};
|
|
222
279
|
/**
|
|
@@ -227,15 +284,44 @@ const dnd = {
|
|
|
227
284
|
* @param targetId 目标节点 id
|
|
228
285
|
* @param position 落点位置
|
|
229
286
|
*/
|
|
230
|
-
const drop = (srcId, targetId, position, e) => {
|
|
287
|
+
const drop = async (srcId, targetId, position, e) => {
|
|
231
288
|
const src = ctx.tree.getNode(srcId);
|
|
232
289
|
const target = ctx.tree.getNode(targetId);
|
|
233
290
|
if (!src || !target) return;
|
|
234
291
|
if (readonly) return;
|
|
235
292
|
const ov = behaviors.get(src.id);
|
|
236
293
|
if (ov?.draggable === false) return;
|
|
237
|
-
if (
|
|
238
|
-
if (position
|
|
294
|
+
if (ov?.sortable === false) return;
|
|
295
|
+
if (position === DropPosition.INSIDE) {
|
|
296
|
+
const rule = ov?.nestable;
|
|
297
|
+
if (rule === false) return;
|
|
298
|
+
if (typeof rule === "function") try {
|
|
299
|
+
const ret = rule(target);
|
|
300
|
+
if (!(ret instanceof Promise ? await ret : ret)) return;
|
|
301
|
+
} catch {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (position !== DropPosition.INSIDE) {
|
|
306
|
+
const sRule = ov?.sortable;
|
|
307
|
+
if (typeof sRule !== "function") {
|
|
308
|
+
if (sRule === false) return;
|
|
309
|
+
} else try {
|
|
310
|
+
const ret = sRule(target);
|
|
311
|
+
if (!(ret instanceof Promise ? await ret : ret)) return;
|
|
312
|
+
} catch {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const dRuleRaw = ov?.draggable;
|
|
316
|
+
if (typeof dRuleRaw !== "function") {
|
|
317
|
+
if (dRuleRaw === false) return;
|
|
318
|
+
} else try {
|
|
319
|
+
const ret = dRuleRaw(target);
|
|
320
|
+
if (!(ret instanceof Promise ? await ret : ret)) return;
|
|
321
|
+
} catch {
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
239
325
|
if (srcId === targetId) return;
|
|
240
326
|
const srcParentId = src.getParent()?.id;
|
|
241
327
|
if (position === DropPosition.INSIDE && srcParentId === targetId) return;
|
|
@@ -445,6 +531,20 @@ const dnd = {
|
|
|
445
531
|
onDragover: (e) => {
|
|
446
532
|
if (!draggingId) return;
|
|
447
533
|
e.preventDefault();
|
|
534
|
+
const draggedIds = getDraggedNodeIds(draggingId);
|
|
535
|
+
for (const did of draggedIds) {
|
|
536
|
+
const ov = behaviors.get(did);
|
|
537
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
538
|
+
containerHoverParentId = void 0;
|
|
539
|
+
containerHoverPos = void 0;
|
|
540
|
+
ghostPos = void 0;
|
|
541
|
+
ctx.events.emit({
|
|
542
|
+
name: "visibility",
|
|
543
|
+
keys: ctx.tree.expandedKeys()
|
|
544
|
+
});
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
448
548
|
const el = e.currentTarget;
|
|
449
549
|
const rect = el.getBoundingClientRect();
|
|
450
550
|
const y = ("clientY" in e ? e.clientY : 0) - rect.top;
|
|
@@ -479,9 +579,25 @@ const dnd = {
|
|
|
479
579
|
return;
|
|
480
580
|
}
|
|
481
581
|
const draggedIds = getDraggedNodeIds(draggingId);
|
|
582
|
+
for (const did of draggedIds) {
|
|
583
|
+
const ov = behaviors.get(did);
|
|
584
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
585
|
+
hoverPositions.clear();
|
|
586
|
+
draggingId = void 0;
|
|
587
|
+
containerHoverParentId = void 0;
|
|
588
|
+
containerHoverPos = void 0;
|
|
589
|
+
ghostPos = void 0;
|
|
590
|
+
ctx.events.emit({
|
|
591
|
+
name: "visibility",
|
|
592
|
+
keys: ctx.tree.expandedKeys()
|
|
593
|
+
});
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
const draggedIds2 = getDraggedNodeIds(draggingId);
|
|
482
598
|
const parentId2 = containerHoverParentId;
|
|
483
599
|
const rawSiblings = parentId2 ? ctx.tree.getNode(parentId2).getChildren() : ctx.tree.items().filter((n) => !n.getParent());
|
|
484
|
-
const processIds = containerHoverPos === DropPosition.AFTER ? [...
|
|
600
|
+
const processIds = containerHoverPos === DropPosition.AFTER ? [...draggedIds2].reverse() : draggedIds2;
|
|
485
601
|
for (const id of processIds) {
|
|
486
602
|
const siblings = rawSiblings.filter((n) => n.id !== id);
|
|
487
603
|
const firstId = siblings[0]?.id;
|
|
@@ -523,7 +639,23 @@ const dnd = {
|
|
|
523
639
|
};
|
|
524
640
|
};
|
|
525
641
|
const ghostStyle = (opts$1) => {
|
|
526
|
-
if (!draggingId || ghostPos === void 0) return { style: { display: "none" } };
|
|
642
|
+
if (evaluating || !draggingId || ghostPos === void 0) return { style: { display: "none" } };
|
|
643
|
+
try {
|
|
644
|
+
const draggedIds = getDraggedNodeIds(draggingId);
|
|
645
|
+
const targetId = lastHoverTargetId;
|
|
646
|
+
if (targetId ? ctx.tree.getNode(targetId) : void 0) if (ghostPos === DropPosition.INSIDE) {
|
|
647
|
+
for (const did of draggedIds) if (behaviors.get(did)?.nestable === false) return { style: { display: "none" } };
|
|
648
|
+
} else for (const did of draggedIds) {
|
|
649
|
+
const ov = behaviors.get(did);
|
|
650
|
+
if (ov?.sortable === false || ov?.draggable === false) return { style: { display: "none" } };
|
|
651
|
+
}
|
|
652
|
+
else for (const did of draggedIds) {
|
|
653
|
+
const ov = behaviors.get(did);
|
|
654
|
+
if (ov?.sortable === false || ov?.draggable === false) return { style: { display: "none" } };
|
|
655
|
+
}
|
|
656
|
+
} catch {
|
|
657
|
+
return { style: { display: "none" } };
|
|
658
|
+
}
|
|
527
659
|
const usePadding = opts$1?.padding ?? true;
|
|
528
660
|
const useMargin = opts$1?.margin ?? true;
|
|
529
661
|
const leftInset = usePadding ? ghostInsetLeft : 0;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@splicetree/plugin-dnd",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "3.
|
|
4
|
+
"version": "3.1.1",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "michael.cocova@gmail.com",
|
|
7
7
|
"name": "Michael Cocova"
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"access": "public"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@splicetree/core": "3.
|
|
26
|
+
"@splicetree/core": "3.1.1"
|
|
27
27
|
},
|
|
28
28
|
"scripts": {
|
|
29
29
|
"dev": "tsdown --watch",
|
package/src/index.ts
CHANGED
|
@@ -8,13 +8,13 @@ declare module '@splicetree/core' {
|
|
|
8
8
|
interface SpliceTreeConfiguration {
|
|
9
9
|
dnd?: DndOptions
|
|
10
10
|
}
|
|
11
|
-
interface SpliceTreeEventPayloadMap {
|
|
11
|
+
interface SpliceTreeEventPayloadMap<T = unknown> {
|
|
12
12
|
move: {
|
|
13
13
|
index: number
|
|
14
|
-
parent: SpliceTreeNode<
|
|
14
|
+
parent: SpliceTreeNode<T> | undefined
|
|
15
15
|
oldIndex: number
|
|
16
|
-
oldParent: SpliceTreeNode<
|
|
17
|
-
node: SpliceTreeNode<
|
|
16
|
+
oldParent: SpliceTreeNode<T> | undefined
|
|
17
|
+
node: SpliceTreeNode<T>
|
|
18
18
|
event?: DragEvent | MouseEvent
|
|
19
19
|
}
|
|
20
20
|
}
|
|
@@ -110,6 +110,8 @@ export const dnd: SpliceTreePlugin = {
|
|
|
110
110
|
let containerHoverParentId: string | undefined
|
|
111
111
|
let containerHoverPos: DropPosition | undefined
|
|
112
112
|
let lastHoverTargetId: string | undefined
|
|
113
|
+
let evalToken = 0
|
|
114
|
+
let evaluating = false
|
|
113
115
|
const refresh = () => ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
114
116
|
const setGhostByEl = (el: HTMLElement, pos: DropPosition) => {
|
|
115
117
|
ghostTop = el.offsetTop
|
|
@@ -193,11 +195,17 @@ export const dnd: SpliceTreePlugin = {
|
|
|
193
195
|
* @param el 目标节点元素
|
|
194
196
|
* @param e 拖拽/鼠标事件
|
|
195
197
|
*/
|
|
196
|
-
const onDragOver = (id: string, el: HTMLElement, e: DragEvent | MouseEvent) => {
|
|
198
|
+
const onDragOver = async (id: string, el: HTMLElement, e: DragEvent | MouseEvent) => {
|
|
197
199
|
// 如果没有拖拽源,直接返回
|
|
198
200
|
if (!draggingId) {
|
|
199
201
|
return
|
|
200
202
|
}
|
|
203
|
+
// 临时隐藏 ghost,直到当前目标判定通过,避免异步判定期间的闪烁
|
|
204
|
+
evalToken++
|
|
205
|
+
const myToken = evalToken
|
|
206
|
+
evaluating = true
|
|
207
|
+
ghostPos = undefined
|
|
208
|
+
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
201
209
|
|
|
202
210
|
const draggedIds = getDraggedNodeIds(draggingId)
|
|
203
211
|
const targetNode = ctx.tree.getNode(id)
|
|
@@ -205,6 +213,7 @@ export const dnd: SpliceTreePlugin = {
|
|
|
205
213
|
// 1. 目标节点不能是拖拽节点之一
|
|
206
214
|
if (draggedIds.includes(id)) {
|
|
207
215
|
hoverPositions.delete(id)
|
|
216
|
+
ghostPos = undefined
|
|
208
217
|
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
209
218
|
return
|
|
210
219
|
}
|
|
@@ -214,6 +223,7 @@ export const dnd: SpliceTreePlugin = {
|
|
|
214
223
|
while (ancestor) {
|
|
215
224
|
if (draggedIds.includes(ancestor.id)) {
|
|
216
225
|
hoverPositions.delete(id)
|
|
226
|
+
ghostPos = undefined
|
|
217
227
|
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
218
228
|
return
|
|
219
229
|
}
|
|
@@ -225,7 +235,9 @@ export const dnd: SpliceTreePlugin = {
|
|
|
225
235
|
|
|
226
236
|
if (!targetNode || pos === undefined) {
|
|
227
237
|
hoverPositions.delete(id)
|
|
238
|
+
ghostPos = undefined
|
|
228
239
|
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
240
|
+
evaluating = false
|
|
229
241
|
return
|
|
230
242
|
}
|
|
231
243
|
|
|
@@ -243,27 +255,93 @@ export const dnd: SpliceTreePlugin = {
|
|
|
243
255
|
return
|
|
244
256
|
}
|
|
245
257
|
|
|
246
|
-
|
|
258
|
+
let allNestable = true
|
|
259
|
+
for (const dId of draggedIds) {
|
|
247
260
|
const ov = behaviors.get(dId)
|
|
248
|
-
|
|
249
|
-
|
|
261
|
+
const rule = ov?.nestable
|
|
262
|
+
if (rule === false) {
|
|
263
|
+
allNestable = false
|
|
264
|
+
break
|
|
265
|
+
}
|
|
266
|
+
if (typeof rule === 'function') {
|
|
267
|
+
try {
|
|
268
|
+
const ret = rule(targetNode)
|
|
269
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
270
|
+
if (!ok) {
|
|
271
|
+
allNestable = false
|
|
272
|
+
break
|
|
273
|
+
}
|
|
274
|
+
} catch {
|
|
275
|
+
allNestable = false
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
250
280
|
if (!allNestable) {
|
|
251
281
|
hoverPositions.delete(id)
|
|
282
|
+
ghostPos = undefined
|
|
252
283
|
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
284
|
+
evaluating = false
|
|
253
285
|
return
|
|
254
286
|
}
|
|
255
287
|
} else {
|
|
256
|
-
|
|
288
|
+
// BEFORE/AFTER:检查 sortable 与 draggable 的目标相关规则
|
|
289
|
+
let allSortable = true
|
|
290
|
+
for (const dId of draggedIds) {
|
|
257
291
|
const ov = behaviors.get(dId)
|
|
258
|
-
|
|
259
|
-
|
|
292
|
+
const sRule = ov?.sortable
|
|
293
|
+
if (typeof sRule === 'boolean' && sRule === false) {
|
|
294
|
+
allSortable = false
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
if (typeof sRule === 'function') {
|
|
298
|
+
try {
|
|
299
|
+
const ret = sRule(targetNode!)
|
|
300
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
301
|
+
if (!ok) {
|
|
302
|
+
allSortable = false
|
|
303
|
+
break
|
|
304
|
+
}
|
|
305
|
+
} catch {
|
|
306
|
+
allSortable = false
|
|
307
|
+
break
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
const dRuleRaw = ov?.draggable
|
|
311
|
+
if (typeof dRuleRaw !== 'function') {
|
|
312
|
+
const dBool = dRuleRaw as boolean | undefined
|
|
313
|
+
if (dBool === false) {
|
|
314
|
+
allSortable = false
|
|
315
|
+
break
|
|
316
|
+
}
|
|
317
|
+
} else {
|
|
318
|
+
try {
|
|
319
|
+
const ret = dRuleRaw(targetNode!)
|
|
320
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
321
|
+
if (!ok) {
|
|
322
|
+
allSortable = false
|
|
323
|
+
break
|
|
324
|
+
}
|
|
325
|
+
} catch {
|
|
326
|
+
allSortable = false
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
260
331
|
if (!allSortable) {
|
|
261
332
|
hoverPositions.delete(id)
|
|
333
|
+
ghostPos = undefined
|
|
262
334
|
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
335
|
+
evaluating = false
|
|
263
336
|
return
|
|
264
337
|
}
|
|
265
338
|
}
|
|
266
339
|
|
|
340
|
+
// 若期间目标已变化或被新的悬停替代,则忽略当前结果
|
|
341
|
+
if (myToken !== evalToken || !draggingId) {
|
|
342
|
+
evaluating = false
|
|
343
|
+
return
|
|
344
|
+
}
|
|
267
345
|
hoverPositions.set(id, pos)
|
|
268
346
|
setGhostByEl(el, pos)
|
|
269
347
|
lastHoverTargetId = id
|
|
@@ -280,6 +358,7 @@ export const dnd: SpliceTreePlugin = {
|
|
|
280
358
|
ghostMarginLeft = 0
|
|
281
359
|
ghostMarginRight = 0
|
|
282
360
|
}
|
|
361
|
+
evaluating = false
|
|
283
362
|
refresh()
|
|
284
363
|
}
|
|
285
364
|
|
|
@@ -287,32 +366,16 @@ export const dnd: SpliceTreePlugin = {
|
|
|
287
366
|
* 离开目标:清理该目标的悬停位置并刷新视图
|
|
288
367
|
* @param id 目标节点 id
|
|
289
368
|
*/
|
|
290
|
-
const onDragLeave = (id: string,
|
|
369
|
+
const onDragLeave = (id: string, _e?: DragEvent) => {
|
|
291
370
|
if (!draggingId) {
|
|
292
371
|
return
|
|
293
372
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return
|
|
299
|
-
}
|
|
300
|
-
const rect = el.getBoundingClientRect()
|
|
301
|
-
const cy = e?.clientY
|
|
302
|
-
if (typeof cy === 'number') {
|
|
303
|
-
if (cy < rect.top) {
|
|
304
|
-
// 上外部:归并为 BEFORE
|
|
305
|
-
hoverPositions.set(id, DropPosition.BEFORE)
|
|
306
|
-
setGhostByEl(el, DropPosition.BEFORE)
|
|
307
|
-
} else if (cy > rect.bottom) {
|
|
308
|
-
// 下外部:归并为 AFTER
|
|
309
|
-
hoverPositions.set(id, DropPosition.AFTER)
|
|
310
|
-
setGhostByEl(el, DropPosition.AFTER)
|
|
311
|
-
} else {
|
|
312
|
-
// 水平离开或无法判断:保留原有位置,不删除
|
|
313
|
-
}
|
|
373
|
+
// 离开目标时直接清理该目标的悬停与 ghost,避免跨节点移动时的占位闪烁
|
|
374
|
+
hoverPositions.delete(id)
|
|
375
|
+
if (lastHoverTargetId === id) {
|
|
376
|
+
lastHoverTargetId = undefined as any
|
|
314
377
|
}
|
|
315
|
-
|
|
378
|
+
ghostPos = undefined
|
|
316
379
|
refresh()
|
|
317
380
|
}
|
|
318
381
|
|
|
@@ -324,7 +387,7 @@ export const dnd: SpliceTreePlugin = {
|
|
|
324
387
|
* @param targetId 目标节点 id
|
|
325
388
|
* @param position 落点位置
|
|
326
389
|
*/
|
|
327
|
-
const drop = (srcId: string, targetId: string, position: DropPosition, e?: DragEvent | MouseEvent) => {
|
|
390
|
+
const drop = async (srcId: string, targetId: string, position: DropPosition, e?: DragEvent | MouseEvent) => {
|
|
328
391
|
const src = ctx.tree.getNode(srcId)
|
|
329
392
|
const target = ctx.tree.getNode(targetId)
|
|
330
393
|
if (!src || !target) {
|
|
@@ -337,11 +400,62 @@ export const dnd: SpliceTreePlugin = {
|
|
|
337
400
|
if (ov?.draggable === false) {
|
|
338
401
|
return
|
|
339
402
|
}
|
|
340
|
-
if (
|
|
403
|
+
if (ov?.sortable === false) {
|
|
341
404
|
return
|
|
342
405
|
}
|
|
343
|
-
if (position
|
|
344
|
-
|
|
406
|
+
if (position === DropPosition.INSIDE) {
|
|
407
|
+
const rule = ov?.nestable
|
|
408
|
+
if (rule === false) {
|
|
409
|
+
return
|
|
410
|
+
}
|
|
411
|
+
if (typeof rule === 'function') {
|
|
412
|
+
try {
|
|
413
|
+
const ret = rule(target)
|
|
414
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
415
|
+
if (!ok) {
|
|
416
|
+
return
|
|
417
|
+
}
|
|
418
|
+
} catch {
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
// BEFORE/AFTER:再次检查 sortable 与 draggable 的目标相关规则,防止误投放
|
|
424
|
+
if (position !== DropPosition.INSIDE) {
|
|
425
|
+
const sRule = ov?.sortable
|
|
426
|
+
if (typeof sRule !== 'function') {
|
|
427
|
+
const sBool = sRule as boolean | undefined
|
|
428
|
+
if (sBool === false) {
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
} else {
|
|
432
|
+
try {
|
|
433
|
+
const ret = sRule(target)
|
|
434
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
435
|
+
if (!ok) {
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
} catch {
|
|
439
|
+
return
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
const dRuleRaw = ov?.draggable
|
|
443
|
+
if (typeof dRuleRaw !== 'function') {
|
|
444
|
+
const dBool = dRuleRaw as boolean | undefined
|
|
445
|
+
if (dBool === false) {
|
|
446
|
+
return
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
try {
|
|
450
|
+
const ret = dRuleRaw(target)
|
|
451
|
+
const ok = ret instanceof Promise ? await ret : ret
|
|
452
|
+
if (!ok) {
|
|
453
|
+
return
|
|
454
|
+
}
|
|
455
|
+
} catch {
|
|
456
|
+
return
|
|
457
|
+
}
|
|
458
|
+
}
|
|
345
459
|
}
|
|
346
460
|
// 基础非法:不能拖到自身或其父
|
|
347
461
|
if (srcId === targetId) {
|
|
@@ -554,6 +668,18 @@ export const dnd: SpliceTreePlugin = {
|
|
|
554
668
|
return
|
|
555
669
|
}
|
|
556
670
|
e.preventDefault()
|
|
671
|
+
// 顶级容器处悬停:若有任何被拖节点声明了布尔 false 的 sortable/draggable,则不显示 ghost
|
|
672
|
+
const draggedIds = getDraggedNodeIds(draggingId)
|
|
673
|
+
for (const did of draggedIds) {
|
|
674
|
+
const ov = behaviors.get(did)
|
|
675
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
676
|
+
containerHoverParentId = undefined
|
|
677
|
+
containerHoverPos = undefined
|
|
678
|
+
ghostPos = undefined
|
|
679
|
+
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
680
|
+
return
|
|
681
|
+
}
|
|
682
|
+
}
|
|
557
683
|
const el = e.currentTarget as HTMLElement
|
|
558
684
|
const rect = el.getBoundingClientRect()
|
|
559
685
|
const y = ('clientY' in e ? e.clientY : 0) - rect.top
|
|
@@ -585,10 +711,24 @@ export const dnd: SpliceTreePlugin = {
|
|
|
585
711
|
ghostPos = undefined
|
|
586
712
|
return
|
|
587
713
|
}
|
|
714
|
+
// 顶级释放:布尔 false 的 sortable/draggable 禁止投放
|
|
588
715
|
const draggedIds = getDraggedNodeIds(draggingId)
|
|
716
|
+
for (const did of draggedIds) {
|
|
717
|
+
const ov = behaviors.get(did)
|
|
718
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
719
|
+
hoverPositions.clear()
|
|
720
|
+
draggingId = undefined
|
|
721
|
+
containerHoverParentId = undefined
|
|
722
|
+
containerHoverPos = undefined
|
|
723
|
+
ghostPos = undefined
|
|
724
|
+
ctx.events.emit({ name: 'visibility', keys: ctx.tree.expandedKeys() })
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
const draggedIds2 = getDraggedNodeIds(draggingId)
|
|
589
729
|
const parentId2 = containerHoverParentId
|
|
590
730
|
const rawSiblings = parentId2 ? ctx.tree.getNode(parentId2)!.getChildren() : ctx.tree.items().filter(n => !n.getParent())
|
|
591
|
-
const processIds = containerHoverPos === DropPosition.AFTER ? [...
|
|
731
|
+
const processIds = containerHoverPos === DropPosition.AFTER ? [...draggedIds2].reverse() : draggedIds2
|
|
592
732
|
for (const id of processIds) {
|
|
593
733
|
const siblings = rawSiblings.filter(n => n.id !== id)
|
|
594
734
|
const firstId = siblings[0]?.id
|
|
@@ -621,7 +761,41 @@ export const dnd: SpliceTreePlugin = {
|
|
|
621
761
|
}
|
|
622
762
|
|
|
623
763
|
const ghostStyle = (opts?: { padding?: boolean, margin?: boolean }) => {
|
|
624
|
-
|
|
764
|
+
// 当被拖拽节点在当前上下文被禁止(sortable/draggable/nestable 为 false)时,不显示 ghost
|
|
765
|
+
if (evaluating || !draggingId || ghostPos === undefined) {
|
|
766
|
+
return { style: { display: 'none' } }
|
|
767
|
+
}
|
|
768
|
+
try {
|
|
769
|
+
const draggedIds = getDraggedNodeIds(draggingId)
|
|
770
|
+
const targetId = lastHoverTargetId
|
|
771
|
+
const targetNode = targetId ? ctx.tree.getNode(targetId) : undefined
|
|
772
|
+
// Node 目标场景:根据落点类型进行规则校验(仅快速检查布尔 false;函数规则已在 onDragOver 中处理为不设置 ghostPos)
|
|
773
|
+
if (targetNode) {
|
|
774
|
+
if (ghostPos === DropPosition.INSIDE) {
|
|
775
|
+
for (const did of draggedIds) {
|
|
776
|
+
const ov = behaviors.get(did)
|
|
777
|
+
if (ov?.nestable === false) {
|
|
778
|
+
return { style: { display: 'none' } }
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} else {
|
|
782
|
+
for (const did of draggedIds) {
|
|
783
|
+
const ov = behaviors.get(did)
|
|
784
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
785
|
+
return { style: { display: 'none' } }
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
} else {
|
|
790
|
+
// 容器场景:无目标节点,只对布尔 false 做拦截(函数规则需节点上下文)
|
|
791
|
+
for (const did of draggedIds) {
|
|
792
|
+
const ov = behaviors.get(did)
|
|
793
|
+
if (ov?.sortable === false || ov?.draggable === false) {
|
|
794
|
+
return { style: { display: 'none' } }
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
} catch {
|
|
625
799
|
return { style: { display: 'none' } }
|
|
626
800
|
}
|
|
627
801
|
const usePadding = opts?.padding ?? true
|
package/src/types.ts
CHANGED
|
@@ -1,16 +1,35 @@
|
|
|
1
1
|
/** dnd types */
|
|
2
2
|
|
|
3
|
+
import type { SpliceTreeNode } from '@splicetree/core'
|
|
4
|
+
|
|
3
5
|
export interface DndNode {
|
|
4
6
|
id: string
|
|
5
7
|
level: number
|
|
6
8
|
parentId?: string
|
|
7
9
|
root?: boolean
|
|
8
10
|
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
export type BehaviorPredicate<T = unknown>
|
|
12
|
+
= | boolean
|
|
13
|
+
| ((target: SpliceTreeNode<T>) => boolean | Promise<boolean>)
|
|
14
|
+
export interface DragBehavior<T = unknown> {
|
|
15
|
+
/**
|
|
16
|
+
* 控制是否允许作为拖拽源在当前目标上执行拖放
|
|
17
|
+
* - 布尔:true/false 全局允许或禁止
|
|
18
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
19
|
+
*/
|
|
20
|
+
draggable?: BehaviorPredicate<T>
|
|
21
|
+
/**
|
|
22
|
+
* 控制是否允许在目标节点的前后排序
|
|
23
|
+
* - 布尔:true/false 全局允许或禁止
|
|
24
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
25
|
+
*/
|
|
26
|
+
sortable?: BehaviorPredicate<T>
|
|
27
|
+
/**
|
|
28
|
+
* 控制能否作为子节点拖入
|
|
29
|
+
* - 布尔:true/false 全局允许或禁止
|
|
30
|
+
* - 函数:基于目标节点动态判断;可返回 Promise<boolean>
|
|
31
|
+
*/
|
|
32
|
+
nestable?: BehaviorPredicate<T>
|
|
14
33
|
}
|
|
15
34
|
|
|
16
35
|
export enum DropPosition {
|