@ptahjs/dnd 0.1.0 → 0.1.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/dnd.es.js +3114 -0
- package/package.json +11 -3
- package/dist/dnd.css +0 -2
- package/dist/dnd.js +0 -1709
package/dist/dnd.es.js
ADDED
|
@@ -0,0 +1,3114 @@
|
|
|
1
|
+
(function(){try{const elementStyle=document.createElement('style');elementStyle.appendChild(document.createTextNode("/* 控制点 */\n.draggable-dot-wrap {\n position: absolute;\n inset: 0;\n pointer-events: none;\n z-index: 1;\n box-sizing: border-box;\n}\n\n.draggable-dot {\n display: block;\n position: absolute;\n z-index: 2;\n width: 10px;\n height: 10px;\n border-radius: 50%;\n background: #3a7afe;\n transform: translate(-50%, -50%);\n pointer-events: auto;\n}\n\n.draggable-dot[data-pos=\"tl\"] {\n left: 0%;\n top: 0%;\n cursor: nw-resize;\n}\n\n.draggable-dot[data-pos=\"tm\"] {\n left: 50%;\n top: 0%;\n width: 16px;\n height: 8px;\n border-radius: 8px;\n cursor: n-resize;\n}\n\n.draggable-dot[data-pos=\"tr\"] {\n top: 0%;\n right: 0%;\n transform: translate(50%, -50%);\n cursor: ne-resize;\n}\n\n/* right */\n.draggable-dot[data-pos=\"rm\"] {\n top: 50%;\n right: 0%;\n width: 8px;\n height: 16px;\n border-radius: 8px;\n transform: translate(50%, -50%);\n cursor: e-resize;\n}\n\n/* 下 */\n.draggable-dot[data-pos=\"br\"] {\n right: 0%;\n bottom: 0%;\n transform: translate(50%, 50%);\n cursor: se-resize;\n}\n\n.draggable-dot[data-pos=\"bm\"] {\n left: 50%;\n bottom: 0%;\n width: 16px;\n height: 8px;\n border-radius: 8px;\n transform: translate(-50%, 50%);\n cursor: s-resize;\n}\n\n.draggable-dot[data-pos=\"bl\"] {\n left: 0%;\n bottom: 0%;\n transform: translate(-50%, 50%);\n cursor: sw-resize;\n}\n\n/* 左 */\n.draggable-dot[data-pos=\"lm\"] {\n left: 0%;\n top: 50%;\n width: 8px;\n height: 16px;\n border-radius: 8px;\n cursor: w-resize;\n}\n\n/* 旋转 */\n.draggable-rotate {\n position: absolute;\n top: 0;\n left: 50%;\n transform: translate(-50%, -200%);\n z-index: 2;\n cursor: grab;\n pointer-events: auto;\n}\n\n.draggable-rotate::after {\n content: \"\";\n width: 16px;\n height: 16px;\n display: block;\n background: center/contain no-repeat\n url(\"data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%201024%201024%22%3E%3Cpath%20fill%3D%22%233a7afe%22%20d%3D%22M784.512%20230.272v-50.56a32%2032%200%201%201%2064%200v149.056a32%2032%200%200%201-32%2032H667.52a32%2032%200%201%201%200-64h92.992A320%20320%200%201%200%20524.8%20833.152a320%20320%200%200%200%20320-320h64a384%20384%200%200%201-384%20384%20384%20384%200%200%201-384-384%20384%20384%200%200%201%20643.712-282.88z%22/%3E%3C/svg%3E\");\n}\n.draggable-markline-x,\n.draggable-markline-y {\n position: absolute;\n left: 0;\n top: 0;\n display: none;\n z-index: 9999;\n background-color: #3a7afe;\n}\n\n.draggable-markline-x {\n width: 1px;\n height: 100%;\n}\n\n.draggable-markline-y {\n width: 100%;\n height: 1px;\n}\n.dnd-mirror {\n position: fixed;\n left: 0px;\n top: 0px;\n margin: 0px;\n pointer-events: none;\n z-index: 999999;\n will-change: transform;\n transform: translate3d(0, 0, 0);\n opacity: 0.5;\n background: #fff;\n}/* 变量集中管理 */\r\n.dnd-indicator {\r\n --dnd-color: #3a7afe;\r\n --dnd-dot: 6px;\r\n --dnd-line: 2px;\r\n position: absolute;\r\n pointer-events: none;\r\n background: var(--dnd-color);\r\n display: none;\r\n}\r\n\r\n.dnd-indicator.dnd-indicator-active {\r\n display: block;\r\n}\r\n\r\n/* 合并选择器,移除多余 opacity:0 */\r\n.dnd-indicator::before,\r\n.dnd-indicator::after {\r\n content: '';\r\n position: absolute;\r\n background: var(--dnd-color);\r\n width: var(--dnd-dot);\r\n height: var(--dnd-dot);\r\n border-radius: 50%;\r\n opacity: 0;\r\n transition: opacity .15s;\r\n}\r\n\r\n.dnd-indicator-active::before,\r\n.dnd-indicator-active::after {\r\n opacity: 1;\r\n}\r\n\r\n/* 水平方向共用一套规则 */\r\n.dnd-indicator--top,\r\n.dnd-indicator--bottom {\r\n height: var(--dnd-line);\r\n width: 100%;\r\n left: 0;\r\n}\r\n\r\n.dnd-indicator--top {\r\n top: 0;\r\n}\r\n\r\n.dnd-indicator--bottom {\r\n bottom: 0;\r\n}\r\n\r\n.dnd-indicator--top::before,\r\n.dnd-indicator--bottom::before {\r\n left: -2px;\r\n}\r\n\r\n.dnd-indicator--top::after,\r\n.dnd-indicator--bottom::after {\r\n right: -2px;\r\n}\r\n\r\n.dnd-indicator--top::before,\r\n.dnd-indicator--top::after {\r\n top: -2px;\r\n}\r\n\r\n.dnd-indicator--bottom::before,\r\n.dnd-indicator--bottom::after {\r\n bottom: -2px;\r\n}\r\n\r\n/* 垂直方向共用一套规则 */\r\n.dnd-indicator--left,\r\n.dnd-indicator--right {\r\n width: var(--dnd-line);\r\n height: 100%;\r\n top: 0;\r\n}\r\n\r\n.dnd-indicator--left {\r\n left: 0;\r\n}\r\n\r\n.dnd-indicator--right {\r\n right: 0;\r\n}\r\n\r\n.dnd-indicator--left::before,\r\n.dnd-indicator--left::after {\r\n left: -2px;\r\n}\r\n\r\n.dnd-indicator--right::before,\r\n.dnd-indicator--right::after {\r\n right: -2px;\r\n}\r\n\r\n.dnd-indicator--left::before,\r\n.dnd-indicator--right::before {\r\n top: -2px;\r\n}\r\n\r\n.dnd-indicator--left::after,\r\n.dnd-indicator--right::after {\r\n bottom: -2px;\r\n}.dnd-marquee {\n position: absolute;\n background: rgba(64, 150, 255, 0.1);\n border: 1px solid rgba(64, 150, 255, 0.6);\n pointer-events: none;\n z-index: 9999;\n}\n/*$vite$:1*/"));document.head.appendChild(elementStyle)}catch(e){console.error('PtahJs Style Inject:',e)}})();import { EventDispatcher } from "@ptahjs/shared";
|
|
2
|
+
//#region src/constant.js
|
|
3
|
+
var ATTR = {
|
|
4
|
+
dataAttr: "data",
|
|
5
|
+
copyAttr: "copy",
|
|
6
|
+
dragAttr: "drag",
|
|
7
|
+
dragScopeAttr: "drag-scope",
|
|
8
|
+
dragdropAttr: "dragdrop",
|
|
9
|
+
dropAttr: "drop",
|
|
10
|
+
handleAttr: "drag-handle",
|
|
11
|
+
handleResizeAttr: "drag-handle-resize",
|
|
12
|
+
handleRotateAttr: "drag-handle-rotate",
|
|
13
|
+
namespaceAttr: "namespace",
|
|
14
|
+
dropIndicatorAttr: "drop-indicator",
|
|
15
|
+
ignoreClickAttr: "ignore-click",
|
|
16
|
+
ignoreMirrorAttr: "ignore-mirror",
|
|
17
|
+
resizableAttr: "resizable",
|
|
18
|
+
rotatableAttr: "rotatable",
|
|
19
|
+
scaleRatioAttr: "scale-ratio"
|
|
20
|
+
};
|
|
21
|
+
var CLASS_NAMES = {
|
|
22
|
+
canDrop: "dnd-canDrop",
|
|
23
|
+
noDrop: "dnd-noDrop",
|
|
24
|
+
dragging: "dnd-dragging",
|
|
25
|
+
mirror: "dnd-mirror",
|
|
26
|
+
active: "dnd-active",
|
|
27
|
+
dropIndicator: "dnd-indicator",
|
|
28
|
+
dropIndicatorActive: "dnd-indicator-active",
|
|
29
|
+
indicatorTop: "dnd-indicator--top",
|
|
30
|
+
indicatorRight: "dnd-indicator--right",
|
|
31
|
+
indicatorBottom: "dnd-indicator--bottom",
|
|
32
|
+
indicatorLeft: "dnd-indicator--left",
|
|
33
|
+
marquee: "dnd-marquee"
|
|
34
|
+
};
|
|
35
|
+
var VERT_SPLIT = .5;
|
|
36
|
+
var HORIZ_SPLIT = 1 / 3;
|
|
37
|
+
var THRESHOLDS = {
|
|
38
|
+
MARQUEE_MOVE: 5,
|
|
39
|
+
DRAG_START: 3
|
|
40
|
+
};
|
|
41
|
+
var AUTO_SCROLL = {
|
|
42
|
+
EDGE: 48,
|
|
43
|
+
MIN_SPEED: 180,
|
|
44
|
+
MAX_SPEED: 600,
|
|
45
|
+
MIN_DT: 8,
|
|
46
|
+
MAX_DT: 64,
|
|
47
|
+
DEFAULT_DT: 16.67
|
|
48
|
+
};
|
|
49
|
+
var MARQUEE_OVERLAP_RATIO = .5;
|
|
50
|
+
var CLAMP_TOLERANCE = .5;
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/utils/computeDropRegion.js
|
|
53
|
+
function computeDropRegion(monitor, support) {
|
|
54
|
+
let cacheKey;
|
|
55
|
+
if (support instanceof Set) cacheKey = support.__regionKey ?? (support.__regionKey = Array.from(support).sort().join(","));
|
|
56
|
+
else cacheKey = support;
|
|
57
|
+
const fullCacheKey = `dropRegionFns_${cacheKey || "all"}`;
|
|
58
|
+
if (monitor?.cache?.[fullCacheKey]) return monitor.cache[fullCacheKey];
|
|
59
|
+
const getDropRect = () => {
|
|
60
|
+
if (!monitor.currentDrop) return;
|
|
61
|
+
return monitor.currentDropRect || void 0;
|
|
62
|
+
};
|
|
63
|
+
const needsVertical = !support || support === "all" || support instanceof Set && (support.has("top") || support.has("bottom"));
|
|
64
|
+
const needsHorizontal = !support || support === "all" || support instanceof Set && (support.has("left") || support.has("right"));
|
|
65
|
+
const fns = {
|
|
66
|
+
isOverTop: needsVertical ? () => {
|
|
67
|
+
const r = getDropRect();
|
|
68
|
+
return r ? monitor.y < r.y + r.height * VERT_SPLIT : false;
|
|
69
|
+
} : () => false,
|
|
70
|
+
isOverBottom: needsVertical ? () => {
|
|
71
|
+
const r = getDropRect();
|
|
72
|
+
return r ? monitor.y > r.y + r.height * (1 - VERT_SPLIT) : false;
|
|
73
|
+
} : () => false,
|
|
74
|
+
isOverLeft: needsHorizontal ? () => {
|
|
75
|
+
const r = getDropRect();
|
|
76
|
+
return r ? monitor.x < r.x + r.width * HORIZ_SPLIT : false;
|
|
77
|
+
} : () => false,
|
|
78
|
+
isOverRight: needsHorizontal ? () => {
|
|
79
|
+
const r = getDropRect();
|
|
80
|
+
return r ? monitor.x > r.x + r.width * (1 - HORIZ_SPLIT) : false;
|
|
81
|
+
} : () => false
|
|
82
|
+
};
|
|
83
|
+
if (monitor?.cache) monitor.cache[fullCacheKey] = fns;
|
|
84
|
+
return fns;
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
//#region src/utils/parseData.js
|
|
88
|
+
/** 解析 data 属性内容(支持 JSON) */
|
|
89
|
+
function parseData(raw) {
|
|
90
|
+
if (raw === void 0) return;
|
|
91
|
+
const s = String(raw).trim();
|
|
92
|
+
if (!s) return "";
|
|
93
|
+
try {
|
|
94
|
+
return JSON.parse(s);
|
|
95
|
+
} catch {
|
|
96
|
+
return s;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
//#endregion
|
|
100
|
+
//#region src/utils/createPayload.js
|
|
101
|
+
function createPayload(type, monitor) {
|
|
102
|
+
const getDropWorldPoint = () => {
|
|
103
|
+
const drop = monitor.currentDrop;
|
|
104
|
+
if (!drop) return void 0;
|
|
105
|
+
const rect = monitor.currentDropRect || drop.getBoundingClientRect();
|
|
106
|
+
const raw = monitor.adapter.getAttr(drop, ATTR.scaleRatioAttr) ?? drop.getAttribute?.("data-scale-ratio");
|
|
107
|
+
const n = Number(raw);
|
|
108
|
+
const scale = Number.isFinite(n) && n > 0 ? n : 1;
|
|
109
|
+
const left = rect.left + (drop.clientLeft || 0);
|
|
110
|
+
const top = rect.top + (drop.clientTop || 0);
|
|
111
|
+
const px = monitor.x - left;
|
|
112
|
+
const py = monitor.y - top;
|
|
113
|
+
const localPx = px + (drop.scrollLeft || 0);
|
|
114
|
+
const localPy = py + (drop.scrollTop || 0);
|
|
115
|
+
return {
|
|
116
|
+
x: localPx / scale,
|
|
117
|
+
y: localPy / scale,
|
|
118
|
+
scale,
|
|
119
|
+
px: localPx,
|
|
120
|
+
py: localPy
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
const getDropData = () => {
|
|
124
|
+
const drop = monitor.currentDrop;
|
|
125
|
+
const cache = monitor.cache;
|
|
126
|
+
if (!drop) {
|
|
127
|
+
cache.dropDataEl = void 0;
|
|
128
|
+
cache.dropDataRaw = void 0;
|
|
129
|
+
cache.dropDataParsed = void 0;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const raw = monitor.adapter.getAttr(drop, ATTR.dataAttr);
|
|
133
|
+
if (cache.dropDataEl === drop && raw === cache.dropDataRaw) return cache.dropDataParsed;
|
|
134
|
+
cache.dropDataEl = drop;
|
|
135
|
+
cache.dropDataRaw = raw;
|
|
136
|
+
cache.dropDataParsed = parseData(raw);
|
|
137
|
+
return cache.dropDataParsed;
|
|
138
|
+
};
|
|
139
|
+
if (!monitor.cache.hitDropFn) {
|
|
140
|
+
let lastFrameId = -1;
|
|
141
|
+
let lastX = 0;
|
|
142
|
+
let lastY = 0;
|
|
143
|
+
let lastIgnore = void 0;
|
|
144
|
+
let lastResult = void 0;
|
|
145
|
+
monitor.cache.hitDropFn = (x, y, ignoreRoot) => {
|
|
146
|
+
const frameId = monitor?.adapter?.frameId ?? monitor?.cache?.frameId ?? 0;
|
|
147
|
+
if (frameId === lastFrameId && x === lastX && y === lastY && ignoreRoot === lastIgnore) return lastResult;
|
|
148
|
+
lastFrameId = frameId;
|
|
149
|
+
lastX = x;
|
|
150
|
+
lastY = y;
|
|
151
|
+
lastIgnore = ignoreRoot;
|
|
152
|
+
const adapter = monitor.adapter;
|
|
153
|
+
if (!adapter?.hitDrop) throw new Error("createPayload requires monitor.adapter.hitDrop");
|
|
154
|
+
lastResult = adapter.hitDrop(x, y, monitor.namespace, ignoreRoot);
|
|
155
|
+
return lastResult;
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
type,
|
|
160
|
+
namespace: monitor.namespace,
|
|
161
|
+
data: monitor.data,
|
|
162
|
+
get dropData() {
|
|
163
|
+
return getDropData();
|
|
164
|
+
},
|
|
165
|
+
get dropPoint() {
|
|
166
|
+
return getDropWorldPoint();
|
|
167
|
+
},
|
|
168
|
+
indicatorRegion: monitor.lastIndicatorRegion,
|
|
169
|
+
isDragScope: monitor.isDragScope,
|
|
170
|
+
isCopy: monitor.isCopy,
|
|
171
|
+
sourceEl: monitor.sourceEl,
|
|
172
|
+
handleEl: monitor.handleEl,
|
|
173
|
+
originDrop: monitor.originDrop,
|
|
174
|
+
currentDrop: monitor.currentDrop,
|
|
175
|
+
x: monitor.x,
|
|
176
|
+
y: monitor.y,
|
|
177
|
+
dx: monitor.dx,
|
|
178
|
+
dy: monitor.dy,
|
|
179
|
+
started: monitor.started,
|
|
180
|
+
hitDrop: monitor.cache.hitDropFn,
|
|
181
|
+
...computeDropRegion(monitor)
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region src/utils/createSession.js
|
|
186
|
+
/**
|
|
187
|
+
* 拖拽 session 状态(一次拖拽过程的运行时容器)
|
|
188
|
+
*
|
|
189
|
+
* 说明:
|
|
190
|
+
* - session 是”短生命周期”:通常在一次 drag 开始创建/复用,结束后 reset。
|
|
191
|
+
* - 为了兼容旧插件,这里保留了一些 legacy 字段(mirror/indicator/cache 等),
|
|
192
|
+
* 同时新的运行时鼓励按 Measure / Compute / Commit 三阶段去组织逻辑。
|
|
193
|
+
*/
|
|
194
|
+
function createSession(store) {
|
|
195
|
+
return {
|
|
196
|
+
/** DOM adapter:由 Dnd 注入(用于命中检测、测量 rect、读写 data/caches 等) */
|
|
197
|
+
adapter: void 0,
|
|
198
|
+
/** 当前命中的 drop 容器 rect(每帧按需测量) */
|
|
199
|
+
currentDropRect: void 0,
|
|
200
|
+
/** 拖拽中止原因/标记(例如外部调用 abort、异常、系统取消等) */
|
|
201
|
+
dragAbort: void 0,
|
|
202
|
+
/** 是否处于”拖拽作用域/拖拽捕获”状态(框架内部用来控制一些分支逻辑) */
|
|
203
|
+
isDragScope: false,
|
|
204
|
+
/** 是否为”复制拖拽”(来自 sourceEl 的 copy=”true/false”) */
|
|
205
|
+
isCopy: false,
|
|
206
|
+
/** 是否处于 active(pointer down 后到 drag end 前) */
|
|
207
|
+
active: false,
|
|
208
|
+
/** pointerId:多指触控/PointerEvent 用于区分指针 */
|
|
209
|
+
pointerId: void 0,
|
|
210
|
+
/** pointer down 时的起始坐标 */
|
|
211
|
+
startX: 0,
|
|
212
|
+
startY: 0,
|
|
213
|
+
/** 当前指针坐标(通常来自 move 事件) */
|
|
214
|
+
x: 0,
|
|
215
|
+
y: 0,
|
|
216
|
+
/** 相对起点位移:dx = x - startX, dy = y - startY */
|
|
217
|
+
dx: 0,
|
|
218
|
+
dy: 0,
|
|
219
|
+
/** 是否已经”真正开始拖拽”(超过阈值后置 true) */
|
|
220
|
+
started: false,
|
|
221
|
+
/** 开始拖拽的阈值(像素),避免轻微抖动误触发 drag */
|
|
222
|
+
threshold: THRESHOLDS.DRAG_START,
|
|
223
|
+
/** 最近一次 move 的时间戳(用于节流/惯性/autoScroll 等逻辑) */
|
|
224
|
+
lastMoveTs: 0,
|
|
225
|
+
/** 被拖拽的源元素(drag source) */
|
|
226
|
+
sourceEl: void 0,
|
|
227
|
+
/** 触发拖拽的 handle(如果支持把手拖拽) */
|
|
228
|
+
handleEl: void 0,
|
|
229
|
+
/** 拖拽开始时所在的 drop(原始容器/起始落点) */
|
|
230
|
+
originDrop: void 0,
|
|
231
|
+
/** 当前命中的 drop(命中检测结果) */
|
|
232
|
+
currentDrop: void 0,
|
|
233
|
+
/** namespace:用于隔离不同拖拽域(不同画布/列表间互不影响) */
|
|
234
|
+
namespace: void 0,
|
|
235
|
+
/** data:本次拖拽携带的业务数据(payload) */
|
|
236
|
+
data: void 0,
|
|
237
|
+
/** 镜像元素(拖拽时跟随指针移动的预览) */
|
|
238
|
+
mirrorEl: void 0,
|
|
239
|
+
/** 镜像相对指针的偏移(通常用于保持抓取点一致) */
|
|
240
|
+
mirrorOffsetX: 0,
|
|
241
|
+
mirrorOffsetY: 0,
|
|
242
|
+
/** 当前是否允许 drop(canDrop 计算结果,供 UI/indicator 使用) */
|
|
243
|
+
currentAllowed: false,
|
|
244
|
+
/** 本帧是否有变更需要在 rAF 里处理(驱动 Measure/Compute/Commit) */
|
|
245
|
+
dirty: false,
|
|
246
|
+
/** 上一帧坐标(用于计算本帧是否移动、速度等) */
|
|
247
|
+
prevX: 0,
|
|
248
|
+
prevY: 0,
|
|
249
|
+
cache: {
|
|
250
|
+
/** 帧 id(自增),标识当前处于第几帧处理 */
|
|
251
|
+
frameId: 0,
|
|
252
|
+
/** drop 数据所在元素(比如 data-* 的宿主元素) */
|
|
253
|
+
dropDataEl: void 0,
|
|
254
|
+
/** drop 原始数据(字符串/对象等原始读取结果) */
|
|
255
|
+
dropDataRaw: void 0,
|
|
256
|
+
/** drop 解析后数据(结构化对象) */
|
|
257
|
+
dropDataParsed: void 0,
|
|
258
|
+
/** drop 区域函数列表/集合(用于自定义命中区域) */
|
|
259
|
+
dropRegionFns: void 0,
|
|
260
|
+
/** 命中检测函数缓存(加速 hitDrop) */
|
|
261
|
+
hitDropFn: void 0,
|
|
262
|
+
/** canDrop 的上下文对象(避免重复创建) */
|
|
263
|
+
canDropCtx: void 0,
|
|
264
|
+
/** dropRect 是否需要重新测量(drop 变化、布局变化等会置脏) */
|
|
265
|
+
dropRectDirty: false,
|
|
266
|
+
/** 自动滚动计划(例如方向/速度/目标容器) */
|
|
267
|
+
autoScrollPlan: void 0,
|
|
268
|
+
/** 自动滚动上次执行时间(用于节流) */
|
|
269
|
+
autoScrollLastTs: 0
|
|
270
|
+
},
|
|
271
|
+
/** indicator 元素(用于显示插入线/高亮等) */
|
|
272
|
+
indicatorEl: void 0,
|
|
273
|
+
/** indicator 当前所在区域/位置描述(用于渲染插入位置等) */
|
|
274
|
+
indicatorRegion: void 0,
|
|
275
|
+
lastIndicatorRegion: void 0,
|
|
276
|
+
/**
|
|
277
|
+
* 持久化选中集(按 namespace 分组)
|
|
278
|
+
* 注意:这里不是复制数据,而是引用 store 里的 Map,
|
|
279
|
+
* 让多次拖拽 session 之间共享选中状态。
|
|
280
|
+
*/
|
|
281
|
+
selectedByNs: store?.selectedByNs,
|
|
282
|
+
/** 最近一次 drag 的时间戳(可用于双击抑制、节流等) */
|
|
283
|
+
lastDragTs: 0,
|
|
284
|
+
/** 是否抑制下一次 click(防止拖拽结束触发误点击) */
|
|
285
|
+
suppressNextClick: false
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/utils/createContext.js
|
|
290
|
+
function createContext({ dnd, session, store, frame, adapter }) {
|
|
291
|
+
const payloadCache = /* @__PURE__ */ new Map();
|
|
292
|
+
const payload = (type = "drag") => {
|
|
293
|
+
if (payloadCache.has(type)) return payloadCache.get(type);
|
|
294
|
+
const p = createPayload(type, session);
|
|
295
|
+
payloadCache.set(type, p);
|
|
296
|
+
return p;
|
|
297
|
+
};
|
|
298
|
+
return {
|
|
299
|
+
dnd,
|
|
300
|
+
session,
|
|
301
|
+
store,
|
|
302
|
+
frame,
|
|
303
|
+
adapter,
|
|
304
|
+
payload,
|
|
305
|
+
resetPayloadCache() {
|
|
306
|
+
payloadCache.clear();
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
//#endregion
|
|
311
|
+
//#region src/utils/isToggleEnabled.js
|
|
312
|
+
function isToggleEnabled(el, attrName, defaultValue = true) {
|
|
313
|
+
if (!el?.getAttribute) return defaultValue;
|
|
314
|
+
const dataName = `data-${attrName}`;
|
|
315
|
+
const v = el.getAttribute(attrName) ?? el.getAttribute(dataName);
|
|
316
|
+
if (v === null) return defaultValue;
|
|
317
|
+
return String(v).toLowerCase() !== "false";
|
|
318
|
+
}
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/utils/matchHandle.js
|
|
321
|
+
function matchHandle(target, adapter) {
|
|
322
|
+
const draggable = adapter?.closestDraggable?.(target) || target?.closest?.(`[${ATTR.dragAttr}], [${ATTR.dragdropAttr}]`);
|
|
323
|
+
if (!draggable) return;
|
|
324
|
+
const rotatable = isToggleEnabled(draggable, ATTR.rotatableAttr, true);
|
|
325
|
+
const resizable = isToggleEnabled(draggable, ATTR.resizableAttr, true);
|
|
326
|
+
if (rotatable && (draggable.hasAttribute(ATTR.handleRotateAttr) || Boolean(draggable.querySelector?.(`[${ATTR.handleRotateAttr}]`))) || resizable && (draggable.hasAttribute(ATTR.handleResizeAttr) || Boolean(draggable.querySelector?.(`[${ATTR.handleResizeAttr}]`)))) {
|
|
327
|
+
const h = (rotatable ? target.closest?.(`[${ATTR.handleRotateAttr}]`) : null) || (resizable ? target.closest?.(`[${ATTR.handleResizeAttr}]`) : null);
|
|
328
|
+
if (h && draggable.contains(h)) return h;
|
|
329
|
+
return draggable;
|
|
330
|
+
}
|
|
331
|
+
if (draggable.hasAttribute(ATTR.handleAttr) || Boolean(draggable.querySelector?.(`[${ATTR.handleAttr}]`))) {
|
|
332
|
+
const h = target.closest?.(`[${ATTR.handleAttr}]`);
|
|
333
|
+
if (h && draggable.contains(h)) return h;
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
return draggable;
|
|
337
|
+
}
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region src/utils/isHTMLElement.js
|
|
340
|
+
function isHTMLElement(el) {
|
|
341
|
+
return el && typeof el === "object" && el.nodeType === 1;
|
|
342
|
+
}
|
|
343
|
+
//#endregion
|
|
344
|
+
//#region src/utils/isIgnoreClick.js
|
|
345
|
+
/**
|
|
346
|
+
* 检查元素或其祖先是否带有 ignore-click 属性
|
|
347
|
+
* @param {Element} el - 要检查的元素
|
|
348
|
+
* @returns {boolean} 是否应该忽略点击
|
|
349
|
+
*/
|
|
350
|
+
function isIgnoreClick(el) {
|
|
351
|
+
if (!isHTMLElement(el)) return false;
|
|
352
|
+
const name = ATTR.ignoreClickAttr;
|
|
353
|
+
const dataName = `data-${name}`;
|
|
354
|
+
return !!(el.closest?.(`[${name}], [${dataName}]`) || el.hasAttribute?.(name) || el.hasAttribute?.(dataName));
|
|
355
|
+
}
|
|
356
|
+
//#endregion
|
|
357
|
+
//#region src/utils/scopeHelpers.js
|
|
358
|
+
/**
|
|
359
|
+
* drag-scope 属性名
|
|
360
|
+
*/
|
|
361
|
+
var SCOPE_ATTR = ATTR.dragScopeAttr;
|
|
362
|
+
/**
|
|
363
|
+
* drag-scope 选择器(支持标准属性和 data- 前缀)
|
|
364
|
+
*/
|
|
365
|
+
var SCOPE_SEL = `[${SCOPE_ATTR}], [data-${SCOPE_ATTR}]`;
|
|
366
|
+
//#endregion
|
|
367
|
+
//#region src/core/DomAdapter.js
|
|
368
|
+
/**
|
|
369
|
+
* DomAdapter
|
|
370
|
+
*
|
|
371
|
+
* Centralized DOM access + per-frame caching.
|
|
372
|
+
*/
|
|
373
|
+
var DomAdapter = class {
|
|
374
|
+
#frameId = 0;
|
|
375
|
+
#rectCache = /* @__PURE__ */ new WeakMap();
|
|
376
|
+
#nsCache = /* @__PURE__ */ new WeakMap();
|
|
377
|
+
get frameId() {
|
|
378
|
+
return this.#frameId;
|
|
379
|
+
}
|
|
380
|
+
beginFrame() {
|
|
381
|
+
this.#frameId++;
|
|
382
|
+
}
|
|
383
|
+
endFrame() {}
|
|
384
|
+
resolveRoot(root) {
|
|
385
|
+
if (!root) return;
|
|
386
|
+
if (typeof root === "string") {
|
|
387
|
+
const el = document.querySelector(root);
|
|
388
|
+
return el instanceof HTMLElement ? el : void 0;
|
|
389
|
+
}
|
|
390
|
+
return root instanceof HTMLElement ? root : void 0;
|
|
391
|
+
}
|
|
392
|
+
getAttr(el, name) {
|
|
393
|
+
return el?.getAttribute?.(name) ?? void 0;
|
|
394
|
+
}
|
|
395
|
+
getNamespace(el) {
|
|
396
|
+
if (!el) return "_default";
|
|
397
|
+
const cached = this.#nsCache.get(el);
|
|
398
|
+
if (cached && cached.frameId === this.#frameId) return cached.ns;
|
|
399
|
+
const ns = this.getAttr(el, ATTR.namespaceAttr) || "_default";
|
|
400
|
+
this.#nsCache.set(el, {
|
|
401
|
+
frameId: this.#frameId,
|
|
402
|
+
ns
|
|
403
|
+
});
|
|
404
|
+
return ns;
|
|
405
|
+
}
|
|
406
|
+
isDrop(el) {
|
|
407
|
+
if (!el) return false;
|
|
408
|
+
return el.hasAttribute?.(ATTR.dropAttr) || el.hasAttribute?.(ATTR.dragdropAttr);
|
|
409
|
+
}
|
|
410
|
+
closestDraggable(el) {
|
|
411
|
+
const { dragAttr, dragdropAttr } = ATTR;
|
|
412
|
+
if (!el) return;
|
|
413
|
+
if (el.hasAttribute(dragAttr) || el.hasAttribute(dragdropAttr)) return el;
|
|
414
|
+
return el.closest?.(`[${dragAttr}], [${dragdropAttr}]`) || void 0;
|
|
415
|
+
}
|
|
416
|
+
closestDrop(el) {
|
|
417
|
+
if (el) {
|
|
418
|
+
const { dropAttr, dragdropAttr } = ATTR;
|
|
419
|
+
return el.closest?.(`[${dropAttr}], [${dragdropAttr}]`) || void 0;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
closestDragScope(el) {
|
|
423
|
+
const { dragScopeAttr } = ATTR;
|
|
424
|
+
if (!el) return;
|
|
425
|
+
if (el.hasAttribute(dragScopeAttr)) return el;
|
|
426
|
+
return el.closest?.(`[${dragScopeAttr}]`) || void 0;
|
|
427
|
+
}
|
|
428
|
+
measureRect(el) {
|
|
429
|
+
if (!el) return;
|
|
430
|
+
const cached = this.#rectCache.get(el);
|
|
431
|
+
if (cached && cached.frameId === this.#frameId) return cached.rect;
|
|
432
|
+
const rect = el.getBoundingClientRect();
|
|
433
|
+
this.#rectCache.set(el, {
|
|
434
|
+
frameId: this.#frameId,
|
|
435
|
+
rect
|
|
436
|
+
});
|
|
437
|
+
return rect;
|
|
438
|
+
}
|
|
439
|
+
/**
|
|
440
|
+
* Hit test drop at (x,y) with namespace validation.
|
|
441
|
+
* ignoreRoot: if provided, ignore ignoreRoot and all its descendants.
|
|
442
|
+
*/
|
|
443
|
+
hitDrop(x, y, ns, ignoreRoot) {
|
|
444
|
+
const el = document.elementFromPoint(x, y);
|
|
445
|
+
if (!el) return;
|
|
446
|
+
const targetNs = ns || "_default";
|
|
447
|
+
let p = el;
|
|
448
|
+
if (ignoreRoot && ignoreRoot.contains(p)) {
|
|
449
|
+
p = ignoreRoot.parentElement ?? null;
|
|
450
|
+
if (!p) return void 0;
|
|
451
|
+
}
|
|
452
|
+
while (p) {
|
|
453
|
+
if (this.isDrop(p)) {
|
|
454
|
+
if (this.getNamespace(p) === targetNs) return p;
|
|
455
|
+
}
|
|
456
|
+
p = p.parentElement;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
/**
|
|
460
|
+
* Hit test 加“source-as-drop”特判:
|
|
461
|
+
* 若 sourceEl 本身是 drop 区,且指针仍在其矩形内,则不激活任何 drop(含父级)。
|
|
462
|
+
* onMeasure 与 onPointerUp 共用此判定,避免落点判定与拖拽过程分叉。
|
|
463
|
+
*/
|
|
464
|
+
hitDropForSource(x, y, ns, sourceEl) {
|
|
465
|
+
const drop = this.hitDrop(x, y, ns, sourceEl);
|
|
466
|
+
if (drop && sourceEl && this.isDrop(sourceEl)) {
|
|
467
|
+
const r = this.measureRect(sourceEl);
|
|
468
|
+
if (r && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom) return;
|
|
469
|
+
}
|
|
470
|
+
return drop;
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
//#endregion
|
|
474
|
+
//#region src/core/RafScheduler.js
|
|
475
|
+
/**
|
|
476
|
+
* RafScheduler(rAF 调度器)
|
|
477
|
+
* - 将同一时间段内多次 `request()` 调用合并(coalesce)为一次 requestAnimationFrame。
|
|
478
|
+
* - 每帧回调返回 boolean:是否需要在下一帧继续执行。
|
|
479
|
+
*/
|
|
480
|
+
var RafScheduler = class {
|
|
481
|
+
#running = false;
|
|
482
|
+
#pending = false;
|
|
483
|
+
#rafId = 0;
|
|
484
|
+
#frameCb;
|
|
485
|
+
constructor(frameCb) {
|
|
486
|
+
this.#frameCb = typeof frameCb === "function" ? frameCb : () => false;
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* 发起一次调度请求:
|
|
490
|
+
* - 标记 pending = true
|
|
491
|
+
* - 若尚未运行,则启动 rAF 循环
|
|
492
|
+
* - 若已在运行,则只做合并标记(下一帧会处理)
|
|
493
|
+
*/
|
|
494
|
+
request() {
|
|
495
|
+
this.#pending = true;
|
|
496
|
+
if (this.#running) return;
|
|
497
|
+
this.#running = true;
|
|
498
|
+
this.#rafId = requestAnimationFrame(this.#tick);
|
|
499
|
+
}
|
|
500
|
+
/**
|
|
501
|
+
* 取消调度:
|
|
502
|
+
* - 停止运行
|
|
503
|
+
* - 清空 pending
|
|
504
|
+
* - 若已挂了 rAF,则取消之
|
|
505
|
+
*/
|
|
506
|
+
cancel() {
|
|
507
|
+
if (!this.#running) return;
|
|
508
|
+
this.#running = false;
|
|
509
|
+
this.#pending = false;
|
|
510
|
+
if (this.#rafId) {
|
|
511
|
+
cancelAnimationFrame(this.#rafId);
|
|
512
|
+
this.#rafId = 0;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/**
|
|
516
|
+
* 每帧执行的 tick:
|
|
517
|
+
* - 消耗本帧的 pending
|
|
518
|
+
* - 执行 frameCb,并根据返回值决定是否继续下一帧
|
|
519
|
+
* - 若本帧执行期间又有人 request(),也会继续下一帧
|
|
520
|
+
*/
|
|
521
|
+
#tick = () => {
|
|
522
|
+
if (!this.#running) return;
|
|
523
|
+
this.#pending = false;
|
|
524
|
+
let keepGoing = false;
|
|
525
|
+
try {
|
|
526
|
+
keepGoing = Boolean(this.#frameCb(performance.now()));
|
|
527
|
+
} catch (err) {
|
|
528
|
+
keepGoing = false;
|
|
529
|
+
console.error("[RafScheduler] frame callback error:", err);
|
|
530
|
+
}
|
|
531
|
+
if (!this.#running) return;
|
|
532
|
+
if (keepGoing || this.#pending) {
|
|
533
|
+
this.#rafId = requestAnimationFrame(this.#tick);
|
|
534
|
+
return;
|
|
535
|
+
}
|
|
536
|
+
this.#running = false;
|
|
537
|
+
this.#rafId = 0;
|
|
538
|
+
};
|
|
539
|
+
};
|
|
540
|
+
//#endregion
|
|
541
|
+
//#region src/core/PointerSensor.js
|
|
542
|
+
/**
|
|
543
|
+
* PointerSensor(指针传感器)
|
|
544
|
+
*
|
|
545
|
+
* 职责:
|
|
546
|
+
* - 监听 root 上的 `pointerdown`
|
|
547
|
+
* - 若 consumer 接受本次交互,则在全局绑定 move/up/cancel/blur 监听(会话级监听)
|
|
548
|
+
* - 对 pointermove 做坐标去重(与上一次坐标相同则跳过),只取末点坐标;
|
|
549
|
+
* 真正的时间合并/节流交给上层 rAF 调度
|
|
550
|
+
*/
|
|
551
|
+
var PointerSensor = class {
|
|
552
|
+
#root;
|
|
553
|
+
#abort = new AbortController();
|
|
554
|
+
#active = false;
|
|
555
|
+
#pointerId;
|
|
556
|
+
#lastX = 0;
|
|
557
|
+
#lastY = 0;
|
|
558
|
+
#onDownCb;
|
|
559
|
+
#onMoveCb;
|
|
560
|
+
#onUpCb;
|
|
561
|
+
constructor({ onDown, onMove, onUp } = {}) {
|
|
562
|
+
this.#onDownCb = typeof onDown === "function" ? onDown : void 0;
|
|
563
|
+
this.#onMoveCb = typeof onMove === "function" ? onMove : void 0;
|
|
564
|
+
this.#onUpCb = typeof onUp === "function" ? onUp : void 0;
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* 设置/更换 root:
|
|
568
|
+
* - 若更换 root,会 abort 掉旧的监听并重新绑定
|
|
569
|
+
*/
|
|
570
|
+
setRoot(root) {
|
|
571
|
+
if (root === this.#root) return;
|
|
572
|
+
this.#abort.abort();
|
|
573
|
+
this.#abort = new AbortController();
|
|
574
|
+
this.#root = root;
|
|
575
|
+
if (!this.#root) return;
|
|
576
|
+
this.#root.addEventListener("pointerdown", this.#onDown, {
|
|
577
|
+
passive: false,
|
|
578
|
+
signal: this.#abort.signal
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
/**
|
|
582
|
+
* 销毁:
|
|
583
|
+
* - 解绑 root 监听
|
|
584
|
+
* - 清理会话状态
|
|
585
|
+
*/
|
|
586
|
+
destroy() {
|
|
587
|
+
this.#abort.abort();
|
|
588
|
+
this.#clearSession();
|
|
589
|
+
this.#root = void 0;
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* root 上的 pointerdown 处理:
|
|
593
|
+
* - 交给 consumer 判断是否接受本次交互
|
|
594
|
+
* - 若接受,拿到 pointerId 和 signal,并绑定全局会话监听
|
|
595
|
+
*/
|
|
596
|
+
#onDown = (event) => {
|
|
597
|
+
if (!this.#onDownCb || !this.#root) return;
|
|
598
|
+
const result = this.#onDownCb(event);
|
|
599
|
+
if (!result?.accepted || !result?.signal || !Number.isFinite(result.pointerId)) return;
|
|
600
|
+
this.#active = true;
|
|
601
|
+
this.#pointerId = result.pointerId;
|
|
602
|
+
this.#lastX = event.clientX;
|
|
603
|
+
this.#lastY = event.clientY;
|
|
604
|
+
this.#bindSession(result.signal);
|
|
605
|
+
};
|
|
606
|
+
/**
|
|
607
|
+
* 绑定会话期的全局监听:
|
|
608
|
+
* - 这些监听使用 consumer 提供的 signal 统一管理生命周期
|
|
609
|
+
* - consumer abort 时会自动移除(无需本类手动 removeEventListener)
|
|
610
|
+
*/
|
|
611
|
+
#bindSession(signal) {
|
|
612
|
+
globalThis.addEventListener("pointermove", this.#onMove, {
|
|
613
|
+
passive: false,
|
|
614
|
+
signal
|
|
615
|
+
});
|
|
616
|
+
globalThis.addEventListener("pointerup", this.#onUp, {
|
|
617
|
+
passive: false,
|
|
618
|
+
signal
|
|
619
|
+
});
|
|
620
|
+
globalThis.addEventListener("pointercancel", this.#onUp, {
|
|
621
|
+
passive: false,
|
|
622
|
+
signal
|
|
623
|
+
});
|
|
624
|
+
globalThis.addEventListener("blur", this.#onUp, {
|
|
625
|
+
passive: false,
|
|
626
|
+
signal
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* 清理当前会话状态(不负责解绑全局监听:由 signal 控制)
|
|
631
|
+
*/
|
|
632
|
+
#clearSession() {
|
|
633
|
+
this.#active = false;
|
|
634
|
+
this.#pointerId = void 0;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 全局 pointermove:
|
|
638
|
+
* - 只处理当前会话的 pointerId
|
|
639
|
+
* - 使用 coalesced events(若支持)取最后一个点作为最终坐标
|
|
640
|
+
* - 节流:在 throttle 时间窗内直接忽略
|
|
641
|
+
* - 去重:坐标未变化则忽略
|
|
642
|
+
*/
|
|
643
|
+
#onMove = (event) => {
|
|
644
|
+
if (!this.#active || !this.#onMoveCb) return;
|
|
645
|
+
if (event.pointerId !== this.#pointerId) return;
|
|
646
|
+
const coalesced = event.getCoalescedEvents?.();
|
|
647
|
+
const last = coalesced?.length ? coalesced[coalesced.length - 1] : event;
|
|
648
|
+
const now = performance.now();
|
|
649
|
+
const x = last.clientX;
|
|
650
|
+
const y = last.clientY;
|
|
651
|
+
if (x === this.#lastX && y === this.#lastY) {
|
|
652
|
+
event.preventDefault();
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
this.#lastX = x;
|
|
656
|
+
this.#lastY = y;
|
|
657
|
+
this.#onMoveCb({
|
|
658
|
+
pointerId: event.pointerId,
|
|
659
|
+
x,
|
|
660
|
+
y,
|
|
661
|
+
event,
|
|
662
|
+
now
|
|
663
|
+
});
|
|
664
|
+
event.preventDefault();
|
|
665
|
+
};
|
|
666
|
+
/**
|
|
667
|
+
* 全局 pointerup / pointercancel / blur:
|
|
668
|
+
* - blur 没有 pointerId,需要特殊处理
|
|
669
|
+
* - 先清理会话状态再回调,避免回调内副作用导致重入触发第二次 onUp
|
|
670
|
+
*/
|
|
671
|
+
#onUp = (event) => {
|
|
672
|
+
if (!this.#active || !this.#onUpCb) return;
|
|
673
|
+
if ("pointerId" in event && event.pointerId !== this.#pointerId) return;
|
|
674
|
+
const pointerId = this.#pointerId;
|
|
675
|
+
const reason = event.type;
|
|
676
|
+
this.#clearSession();
|
|
677
|
+
this.#onUpCb({
|
|
678
|
+
event,
|
|
679
|
+
pointerId,
|
|
680
|
+
reason
|
|
681
|
+
});
|
|
682
|
+
event.preventDefault?.();
|
|
683
|
+
};
|
|
684
|
+
};
|
|
685
|
+
//#endregion
|
|
686
|
+
//#region src/core/FrameContext.js
|
|
687
|
+
/**
|
|
688
|
+
* FrameContext(帧上下文)
|
|
689
|
+
*
|
|
690
|
+
* 用于在每一帧中在各个插件之间共享的上下文数据。
|
|
691
|
+
*
|
|
692
|
+
* 插件约定:
|
|
693
|
+
* - 只在 onMeasure 阶段读取 DOM(避免读写交错导致强制回流)
|
|
694
|
+
* - 在 onCompute 阶段做纯计算(不触碰 DOM)
|
|
695
|
+
* - 在 onCommit 阶段把 DOM 写操作通过 frame 入队(集中提交)
|
|
696
|
+
*/
|
|
697
|
+
var FrameContext = class {
|
|
698
|
+
scrolled = false;
|
|
699
|
+
now = 0;
|
|
700
|
+
classOps = /* @__PURE__ */ new Map();
|
|
701
|
+
styleOps = /* @__PURE__ */ new Map();
|
|
702
|
+
appendOps = /* @__PURE__ */ new Map();
|
|
703
|
+
removeOps = /* @__PURE__ */ new Set();
|
|
704
|
+
fnOps = [];
|
|
705
|
+
/**
|
|
706
|
+
* 重置为新的一帧状态
|
|
707
|
+
* @param now 当前帧时间戳
|
|
708
|
+
*/
|
|
709
|
+
reset(now) {
|
|
710
|
+
this.now = now;
|
|
711
|
+
this.scrolled = false;
|
|
712
|
+
this.classOps.clear();
|
|
713
|
+
this.styleOps.clear();
|
|
714
|
+
this.appendOps.clear();
|
|
715
|
+
this.removeOps.clear();
|
|
716
|
+
this.fnOps.length = 0;
|
|
717
|
+
}
|
|
718
|
+
/**
|
|
719
|
+
* 记录一次 class toggle 操作(合并到队列中,last-write-wins)
|
|
720
|
+
*/
|
|
721
|
+
toggleClass(el, className, on) {
|
|
722
|
+
if (!el || !className) return;
|
|
723
|
+
if (this.removeOps.has(el)) return;
|
|
724
|
+
let m = this.classOps.get(el);
|
|
725
|
+
if (!m) {
|
|
726
|
+
m = /* @__PURE__ */ new Map();
|
|
727
|
+
this.classOps.set(el, m);
|
|
728
|
+
}
|
|
729
|
+
m.set(className, Boolean(on));
|
|
730
|
+
}
|
|
731
|
+
/**
|
|
732
|
+
* 记录一次 style 写操作(合并到队列中,last-write-wins)
|
|
733
|
+
*/
|
|
734
|
+
setStyle(el, prop, value) {
|
|
735
|
+
if (!el || !prop) return;
|
|
736
|
+
if (this.removeOps.has(el)) return;
|
|
737
|
+
let m = this.styleOps.get(el);
|
|
738
|
+
if (!m) {
|
|
739
|
+
m = /* @__PURE__ */ new Map();
|
|
740
|
+
this.styleOps.set(el, m);
|
|
741
|
+
}
|
|
742
|
+
m.set(prop, value);
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* 记录一次 append 操作(child 的归属 parent 以最后一次为准)
|
|
746
|
+
*/
|
|
747
|
+
append(parent, child) {
|
|
748
|
+
if (!parent || !child) return;
|
|
749
|
+
if (this.removeOps.has(child)) return;
|
|
750
|
+
this.appendOps.set(child, parent);
|
|
751
|
+
}
|
|
752
|
+
/**
|
|
753
|
+
* 记录一次 remove 操作(remove 优先级最高)
|
|
754
|
+
*/
|
|
755
|
+
remove(el) {
|
|
756
|
+
if (!el) return;
|
|
757
|
+
this.removeOps.add(el);
|
|
758
|
+
this.classOps.delete(el);
|
|
759
|
+
this.styleOps.delete(el);
|
|
760
|
+
this.appendOps.delete(el);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* 记录一个待执行函数(保持顺序,允许重复)
|
|
764
|
+
*/
|
|
765
|
+
run(fn) {
|
|
766
|
+
if (typeof fn !== "function") return;
|
|
767
|
+
this.fnOps.push(fn);
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* 提交本帧所有操作(按固定顺序执行,尽量避免冲突)
|
|
771
|
+
*/
|
|
772
|
+
commit() {
|
|
773
|
+
for (const el of this.removeOps) el?.parentNode?.removeChild?.(el);
|
|
774
|
+
const hasRemovals = this.removeOps.size > 0;
|
|
775
|
+
for (const [child, parent] of this.appendOps) if (!hasRemovals || !this.removeOps.has(child) && !this.removeOps.has(parent)) parent?.appendChild?.(child);
|
|
776
|
+
for (const [el, classes] of this.classOps) if (!hasRemovals || !this.removeOps.has(el)) for (const [cls, on] of classes) if (on) el?.classList?.add?.(cls);
|
|
777
|
+
else el?.classList?.remove?.(cls);
|
|
778
|
+
for (const [el, styles] of this.styleOps) if (!hasRemovals || !this.removeOps.has(el)) {
|
|
779
|
+
for (const [prop, value] of styles) if (value === void 0 || value === null) {
|
|
780
|
+
if (prop.includes("-")) el?.style?.removeProperty?.(prop);
|
|
781
|
+
else if (el?.style) el.style[prop] = "";
|
|
782
|
+
} else if (prop.includes("-")) el?.style?.setProperty?.(prop, String(value));
|
|
783
|
+
else if (el?.style) el.style[prop] = value;
|
|
784
|
+
}
|
|
785
|
+
for (const fn of this.fnOps) fn?.();
|
|
786
|
+
}
|
|
787
|
+
};
|
|
788
|
+
//#endregion
|
|
789
|
+
//#region src/core/PluginRuntime.js
|
|
790
|
+
/**
|
|
791
|
+
* PluginRuntime
|
|
792
|
+
*
|
|
793
|
+
* 插件运行时调度器:负责管理插件的注册、排序,以及在 DnD 生命周期内
|
|
794
|
+
* 按顺序分发各阶段回调(onDown/onStart/onMeasure/onCompute/onCommit/onEnd...)。
|
|
795
|
+
*
|
|
796
|
+
* 设计要点:
|
|
797
|
+
* 1) 插件按 order 升序执行(越小越早)。
|
|
798
|
+
* 2) 所有回调都是可选的(?. 调用)。
|
|
799
|
+
* 3) onAfterDrag 有返回值聚合逻辑:任一插件返回 true 或 { scrolled: true } 都视为发生了滚动。
|
|
800
|
+
*/
|
|
801
|
+
var PluginRuntime = class {
|
|
802
|
+
/**
|
|
803
|
+
* 已注册插件列表(内部私有)
|
|
804
|
+
* @type {Array<any>}
|
|
805
|
+
*/
|
|
806
|
+
#plugins = [];
|
|
807
|
+
/**
|
|
808
|
+
* 注册插件并排序,然后调用插件的 onAttach(用于做初始化、挂事件、扩展 dnd api 等)
|
|
809
|
+
*
|
|
810
|
+
* @param {any} plugin 插件实例(可包含 order、onAttach、各生命周期钩子)
|
|
811
|
+
* @param {any} dnd Dnd 实例(传给插件做初始化)
|
|
812
|
+
*/
|
|
813
|
+
use(plugin, dnd) {
|
|
814
|
+
if (!plugin) return;
|
|
815
|
+
this.#plugins.push(plugin);
|
|
816
|
+
this.#plugins.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
|
|
817
|
+
plugin.onAttach?.(dnd);
|
|
818
|
+
}
|
|
819
|
+
/**
|
|
820
|
+
* root 容器变更通知(比如 dnd.root 被替换)
|
|
821
|
+
* 插件可在此处重新绑定事件或做清理。
|
|
822
|
+
*
|
|
823
|
+
* @param {HTMLElement} nextRoot 新 root
|
|
824
|
+
* @param {HTMLElement} prevRoot 旧 root
|
|
825
|
+
* @param {AbortSignal} signal 用于自动解绑事件的 signal
|
|
826
|
+
*/
|
|
827
|
+
onRootChange(nextRoot, prevRoot, signal) {
|
|
828
|
+
for (const p of this.#plugins) p.onRootChange?.(nextRoot, prevRoot, signal);
|
|
829
|
+
}
|
|
830
|
+
/**
|
|
831
|
+
* pointer/mouse/touch 按下阶段(拖拽尚未开始)
|
|
832
|
+
* 通常用于:命中测试、准备 session、记录初始坐标等。
|
|
833
|
+
*
|
|
834
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
835
|
+
* @param {Event} event 原始事件对象
|
|
836
|
+
*/
|
|
837
|
+
onDown(ctx, event) {
|
|
838
|
+
for (const p of this.#plugins) p.onDown?.(ctx, event);
|
|
839
|
+
}
|
|
840
|
+
/**
|
|
841
|
+
* 真正开始拖拽(跨过 threshold / started=true)阶段
|
|
842
|
+
* 通常用于:创建 mirror、创建 indicator、更新选中态等。
|
|
843
|
+
*
|
|
844
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
845
|
+
*/
|
|
846
|
+
onStart(ctx) {
|
|
847
|
+
for (const p of this.#plugins) p.onStart?.(ctx);
|
|
848
|
+
}
|
|
849
|
+
/**
|
|
850
|
+
* Measure 阶段:只读测量(读 DOM)
|
|
851
|
+
* 通常用于:读取 rect、计算命中 drop、记录布局信息等。
|
|
852
|
+
*
|
|
853
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
854
|
+
*/
|
|
855
|
+
onMeasure(ctx) {
|
|
856
|
+
for (const p of this.#plugins) p.onMeasure?.(ctx);
|
|
857
|
+
}
|
|
858
|
+
/**
|
|
859
|
+
* Compute 阶段:纯计算(不写 DOM)
|
|
860
|
+
* 通常用于:根据 measure 的结果计算 region、allowed、snap 等。
|
|
861
|
+
*
|
|
862
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
863
|
+
*/
|
|
864
|
+
onCompute(ctx) {
|
|
865
|
+
for (const p of this.#plugins) p.onCompute?.(ctx);
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Commit 阶段:写入(写 DOM)
|
|
869
|
+
* 通常用于:更新 class/style、移动 mirror、更新 indicator 形态等。
|
|
870
|
+
*
|
|
871
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
872
|
+
*/
|
|
873
|
+
onCommit(ctx) {
|
|
874
|
+
for (const p of this.#plugins) p.onCommit?.(ctx);
|
|
875
|
+
}
|
|
876
|
+
/**
|
|
877
|
+
* AfterDrag 阶段:拖拽过程中的后置处理(通常在 move 后)
|
|
878
|
+
* 用途:自动滚动(auto scroll)、边缘滚动等。
|
|
879
|
+
*
|
|
880
|
+
* 返回值聚合规则:
|
|
881
|
+
* - 任一插件返回 true 或 { scrolled: true } => 表示发生了滚动
|
|
882
|
+
* - 最终返回 scrolled(boolean)供上层决定是否需要额外刷新/下一帧等
|
|
883
|
+
*
|
|
884
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
885
|
+
* @returns {boolean} 是否发生滚动(或需要视为滚动)
|
|
886
|
+
*/
|
|
887
|
+
onAfterDrag(ctx) {
|
|
888
|
+
let scrolled = false;
|
|
889
|
+
for (const p of this.#plugins) {
|
|
890
|
+
const r = p.onAfterDrag?.(ctx);
|
|
891
|
+
if (r === true || r?.scrolled) scrolled = true;
|
|
892
|
+
}
|
|
893
|
+
return scrolled;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* 拖拽结束阶段
|
|
897
|
+
* 通常用于:清理 DOM(mirror/indicator)、恢复样式、派发 drop 结果等。
|
|
898
|
+
*
|
|
899
|
+
* @param {any} ctx 帧上下文/会话上下文
|
|
900
|
+
* @param {any} meta 结束元信息(比如 drop 成功/取消、原因等)
|
|
901
|
+
*/
|
|
902
|
+
onEnd(ctx, meta) {
|
|
903
|
+
for (const p of this.#plugins) p.onEnd?.(ctx, meta);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Dnd 实例销毁阶段
|
|
907
|
+
* 通常用于:解绑事件、释放引用、兜底清理(防止中途销毁导致残留)。
|
|
908
|
+
*
|
|
909
|
+
* @param {any} dnd Dnd 实例
|
|
910
|
+
* @param {any} session 当前会话/monitor(用于清理残留 DOM)
|
|
911
|
+
*/
|
|
912
|
+
onDestroy(dnd, session) {
|
|
913
|
+
for (const p of this.#plugins) p.onDestroy?.(dnd, session);
|
|
914
|
+
}
|
|
915
|
+
/**
|
|
916
|
+
* 释放插件强引用,断开 plugin <-> dnd 之间的循环引用,便于 GC。
|
|
917
|
+
* 应该在 dnd.destroy() 末尾调用。
|
|
918
|
+
*/
|
|
919
|
+
dispose() {
|
|
920
|
+
this.#plugins.length = 0;
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
//#endregion
|
|
924
|
+
//#region src/core/State.js
|
|
925
|
+
var State = class {
|
|
926
|
+
#store;
|
|
927
|
+
#session;
|
|
928
|
+
constructor({ store, session } = {}) {
|
|
929
|
+
this.#store = store;
|
|
930
|
+
this.#session = session;
|
|
931
|
+
}
|
|
932
|
+
get session() {
|
|
933
|
+
return this.#session;
|
|
934
|
+
}
|
|
935
|
+
get store() {
|
|
936
|
+
return this.#store;
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Dispatch an action to update session state.
|
|
940
|
+
* Returns small effects flags for the facade.
|
|
941
|
+
*/
|
|
942
|
+
dispatch(action) {
|
|
943
|
+
if (!action || !this.#session) return { handled: false };
|
|
944
|
+
switch (action.type) {
|
|
945
|
+
case "DOWN": {
|
|
946
|
+
if (this.#session.active) return { handled: false };
|
|
947
|
+
Object.assign(this.#session, {
|
|
948
|
+
active: true,
|
|
949
|
+
started: false,
|
|
950
|
+
dirty: false,
|
|
951
|
+
pointerId: action.pointerId,
|
|
952
|
+
startX: action.x,
|
|
953
|
+
startY: action.y,
|
|
954
|
+
x: action.x,
|
|
955
|
+
y: action.y,
|
|
956
|
+
prevX: action.x,
|
|
957
|
+
prevY: action.y,
|
|
958
|
+
dx: 0,
|
|
959
|
+
dy: 0,
|
|
960
|
+
namespace: action.namespace,
|
|
961
|
+
data: action.data,
|
|
962
|
+
sourceEl: action.sourceEl,
|
|
963
|
+
handleEl: action.handleEl,
|
|
964
|
+
originDrop: action.originDrop,
|
|
965
|
+
currentDrop: void 0,
|
|
966
|
+
currentDropRect: void 0,
|
|
967
|
+
isDragScope: Boolean(action.isDragScope),
|
|
968
|
+
isCopy: Boolean(action.isCopy),
|
|
969
|
+
mirrorOffsetX: action.mirrorOffsetX ?? 0,
|
|
970
|
+
mirrorOffsetY: action.mirrorOffsetY ?? 0,
|
|
971
|
+
lastMoveTs: 0
|
|
972
|
+
});
|
|
973
|
+
this.#session.dragAbort = new AbortController();
|
|
974
|
+
const { cache } = this.#session;
|
|
975
|
+
if (cache) {
|
|
976
|
+
cache.dropDataEl = void 0;
|
|
977
|
+
cache.dropDataRaw = void 0;
|
|
978
|
+
cache.dropDataParsed = void 0;
|
|
979
|
+
cache.hitDropFn = void 0;
|
|
980
|
+
cache.dropRectDirty = true;
|
|
981
|
+
}
|
|
982
|
+
return {
|
|
983
|
+
handled: true,
|
|
984
|
+
accepted: true,
|
|
985
|
+
dragAbort: this.#session.dragAbort
|
|
986
|
+
};
|
|
987
|
+
}
|
|
988
|
+
case "MOVE": {
|
|
989
|
+
if (!this.#session.active || action.pointerId !== this.#session.pointerId) return { handled: false };
|
|
990
|
+
const nextX = action.x;
|
|
991
|
+
const nextY = action.y;
|
|
992
|
+
if (nextX === this.#session.x && nextY === this.#session.y) return {
|
|
993
|
+
handled: true,
|
|
994
|
+
moved: false
|
|
995
|
+
};
|
|
996
|
+
this.#session.prevX = this.#session.x;
|
|
997
|
+
this.#session.prevY = this.#session.y;
|
|
998
|
+
this.#session.x = nextX;
|
|
999
|
+
this.#session.y = nextY;
|
|
1000
|
+
this.#session.dx = nextX - this.#session.startX;
|
|
1001
|
+
this.#session.dy = nextY - this.#session.startY;
|
|
1002
|
+
this.#session.dirty = true;
|
|
1003
|
+
let startedNow = false;
|
|
1004
|
+
if (!this.#session.started) {
|
|
1005
|
+
if (Math.hypot(this.#session.dx, this.#session.dy) >= this.#session.threshold) {
|
|
1006
|
+
this.#session.started = true;
|
|
1007
|
+
startedNow = true;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
return {
|
|
1011
|
+
handled: true,
|
|
1012
|
+
moved: true,
|
|
1013
|
+
startedNow,
|
|
1014
|
+
started: this.#session.started
|
|
1015
|
+
};
|
|
1016
|
+
}
|
|
1017
|
+
case "END": {
|
|
1018
|
+
if (!this.#session.active) return { handled: false };
|
|
1019
|
+
if (action.pointerId !== void 0 && action.pointerId !== this.#session.pointerId) return { handled: false };
|
|
1020
|
+
const ended = Boolean(this.#session.started);
|
|
1021
|
+
this.#session.active = false;
|
|
1022
|
+
return {
|
|
1023
|
+
handled: true,
|
|
1024
|
+
ended,
|
|
1025
|
+
reason: action.reason
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
default: return { handled: false };
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
/** Reset session after end/destroy, preserving store references. */
|
|
1032
|
+
resetSession({ ended } = { ended: false }) {
|
|
1033
|
+
const store = this.#store;
|
|
1034
|
+
const prev = this.#session;
|
|
1035
|
+
const next = createSession(store);
|
|
1036
|
+
next.adapter = prev?.adapter;
|
|
1037
|
+
if (ended) {
|
|
1038
|
+
next.suppressNextClick = true;
|
|
1039
|
+
next.lastDragTs = performance.now();
|
|
1040
|
+
}
|
|
1041
|
+
this.#session = next;
|
|
1042
|
+
return next;
|
|
1043
|
+
}
|
|
1044
|
+
};
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region src/core/Dnd.js
|
|
1047
|
+
/**
|
|
1048
|
+
* Dnd Facade(对外门面)
|
|
1049
|
+
*
|
|
1050
|
+
* 设计目标:
|
|
1051
|
+
* - 内部拆分职责:sensor(输入采集)/ engine(state)(状态机)/ runtime(插件调度)
|
|
1052
|
+
* / adapter(DOM 适配)/ scheduler(rAF 驱动)
|
|
1053
|
+
*/
|
|
1054
|
+
var Dnd = class extends EventDispatcher {
|
|
1055
|
+
#abort = new AbortController();
|
|
1056
|
+
#root;
|
|
1057
|
+
#config;
|
|
1058
|
+
#adapter = new DomAdapter();
|
|
1059
|
+
#frame = new FrameContext();
|
|
1060
|
+
#store = { selectedByNs: /* @__PURE__ */ new Map() };
|
|
1061
|
+
#state;
|
|
1062
|
+
#runtime = new PluginRuntime();
|
|
1063
|
+
#scheduler;
|
|
1064
|
+
#sensor;
|
|
1065
|
+
#destroyed = false;
|
|
1066
|
+
#frameCtx;
|
|
1067
|
+
canDrop = () => true;
|
|
1068
|
+
renderMirror = void 0;
|
|
1069
|
+
/** 当前 root */
|
|
1070
|
+
get root() {
|
|
1071
|
+
return this.#root;
|
|
1072
|
+
}
|
|
1073
|
+
/** 对外暴露 monitor(当前 session),用于观察拖拽状态 */
|
|
1074
|
+
get monitor() {
|
|
1075
|
+
if (this.#destroyed) return void 0;
|
|
1076
|
+
return this.#state?.session;
|
|
1077
|
+
}
|
|
1078
|
+
/** 是否已销毁(destroy 后只读) */
|
|
1079
|
+
get destroyed() {
|
|
1080
|
+
return this.#destroyed;
|
|
1081
|
+
}
|
|
1082
|
+
constructor(config = {}) {
|
|
1083
|
+
super();
|
|
1084
|
+
this.#config = { threshold: config.threshold ?? THRESHOLDS.DRAG_START };
|
|
1085
|
+
const session = createSession(this.#store);
|
|
1086
|
+
session.threshold = this.#config.threshold;
|
|
1087
|
+
session.adapter = this.#adapter;
|
|
1088
|
+
this.#state = new State({
|
|
1089
|
+
store: this.#store,
|
|
1090
|
+
session
|
|
1091
|
+
});
|
|
1092
|
+
this.#scheduler = new RafScheduler(this.#onFrame);
|
|
1093
|
+
this.#sensor = new PointerSensor({
|
|
1094
|
+
onDown: this.#onPointerDown,
|
|
1095
|
+
onMove: this.#onPointerMove,
|
|
1096
|
+
onUp: this.#onPointerUp
|
|
1097
|
+
});
|
|
1098
|
+
this.setRoot(config.root);
|
|
1099
|
+
}
|
|
1100
|
+
/**
|
|
1101
|
+
* 注册插件(按 order 升序执行)
|
|
1102
|
+
*/
|
|
1103
|
+
use(plugin) {
|
|
1104
|
+
if (!plugin) return this;
|
|
1105
|
+
this.#runtime.use(plugin, this);
|
|
1106
|
+
if (this.#root) plugin.onRootChange?.(this.#root, void 0, this.#abort.signal);
|
|
1107
|
+
return this;
|
|
1108
|
+
}
|
|
1109
|
+
/**
|
|
1110
|
+
* 设置/更换 root
|
|
1111
|
+
* - 会 abort 旧 root 相关监听
|
|
1112
|
+
* - 通知 sensor 绑定 root pointerdown
|
|
1113
|
+
* - 通知 plugins root 变化
|
|
1114
|
+
*/
|
|
1115
|
+
setRoot(root) {
|
|
1116
|
+
const nextRoot = this.#adapter.resolveRoot(root);
|
|
1117
|
+
if (nextRoot === this.#root) return;
|
|
1118
|
+
const prevRoot = this.#root;
|
|
1119
|
+
this.#abort.abort();
|
|
1120
|
+
this.#abort = new AbortController();
|
|
1121
|
+
this.#root = nextRoot;
|
|
1122
|
+
this.#sensor.setRoot(this.#root);
|
|
1123
|
+
if (!this.#root) return;
|
|
1124
|
+
this.#runtime.onRootChange(this.#root, prevRoot, this.#abort.signal);
|
|
1125
|
+
this.#root.addEventListener("click", this.#onClickCapture, {
|
|
1126
|
+
capture: true,
|
|
1127
|
+
passive: false,
|
|
1128
|
+
signal: this.#abort.signal
|
|
1129
|
+
});
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* 销毁:
|
|
1133
|
+
* - 解绑 root 事件、停止 sensor
|
|
1134
|
+
* - 停止 rAF
|
|
1135
|
+
* - 结束活跃 session(不触发 drop/cancel 事件)
|
|
1136
|
+
* - 通知插件销毁
|
|
1137
|
+
* - 重置 session
|
|
1138
|
+
*/
|
|
1139
|
+
destroy() {
|
|
1140
|
+
if (this.#destroyed) return;
|
|
1141
|
+
this.#abort.abort();
|
|
1142
|
+
this.#sensor.destroy();
|
|
1143
|
+
this.#scheduler.cancel();
|
|
1144
|
+
const monitor = this.#state.session;
|
|
1145
|
+
if (monitor?.active) {
|
|
1146
|
+
this.#runtime.onEnd(createContext({
|
|
1147
|
+
dnd: this,
|
|
1148
|
+
session: monitor,
|
|
1149
|
+
store: this.#store,
|
|
1150
|
+
frame: this.#frame,
|
|
1151
|
+
adapter: this.#adapter
|
|
1152
|
+
}), {
|
|
1153
|
+
ended: false,
|
|
1154
|
+
reason: "destroy"
|
|
1155
|
+
});
|
|
1156
|
+
monitor.dragAbort?.abort();
|
|
1157
|
+
}
|
|
1158
|
+
this.#runtime.onDestroy(this, monitor);
|
|
1159
|
+
this.#runtime.dispose?.();
|
|
1160
|
+
this.#frameCtx = void 0;
|
|
1161
|
+
this.offAll();
|
|
1162
|
+
this.#destroyed = true;
|
|
1163
|
+
}
|
|
1164
|
+
#onClickCapture = (e) => {
|
|
1165
|
+
const m = this.#state?.session;
|
|
1166
|
+
if (!m?.suppressNextClick) return;
|
|
1167
|
+
const now = performance.now();
|
|
1168
|
+
const withinWindow = !m.lastDragTs || now - m.lastDragTs < 800;
|
|
1169
|
+
m.suppressNextClick = false;
|
|
1170
|
+
if (!withinWindow) return;
|
|
1171
|
+
e.preventDefault?.();
|
|
1172
|
+
e.stopPropagation?.();
|
|
1173
|
+
e.stopImmediatePropagation?.();
|
|
1174
|
+
};
|
|
1175
|
+
/**
|
|
1176
|
+
* pointerdown:
|
|
1177
|
+
* - 校验 root、按键、handle、draggable
|
|
1178
|
+
* - dispatch DOWN 到状态机
|
|
1179
|
+
* - 执行插件 onDown(允许入队 DOM 写操作)
|
|
1180
|
+
* - 返回给 sensor:accepted/pointerId/signal,用于建立会话监听
|
|
1181
|
+
*/
|
|
1182
|
+
#onPointerDown = (event) => {
|
|
1183
|
+
if (!this.#root) return { accepted: false };
|
|
1184
|
+
if (event.button !== 0) return { accepted: false };
|
|
1185
|
+
const handle = matchHandle(event.target, this.#adapter);
|
|
1186
|
+
if (!handle) return { accepted: false };
|
|
1187
|
+
const draggable = this.#adapter.closestDraggable(handle);
|
|
1188
|
+
if (!draggable) return { accepted: false };
|
|
1189
|
+
if (event.target?.closest?.("input, textarea, select, button, a[href], [contenteditable]")) return { accepted: false };
|
|
1190
|
+
event.preventDefault();
|
|
1191
|
+
event.target?.setPointerCapture?.(event.pointerId);
|
|
1192
|
+
const ns = this.#adapter.getNamespace(draggable);
|
|
1193
|
+
const originDrop = this.#adapter.closestDrop(draggable);
|
|
1194
|
+
const rect = draggable.getBoundingClientRect();
|
|
1195
|
+
const data = parseData(this.#adapter.getAttr(draggable, ATTR.dataAttr));
|
|
1196
|
+
if (!this.#state.dispatch({
|
|
1197
|
+
type: "DOWN",
|
|
1198
|
+
pointerId: event.pointerId,
|
|
1199
|
+
x: event.clientX,
|
|
1200
|
+
y: event.clientY,
|
|
1201
|
+
namespace: ns,
|
|
1202
|
+
data,
|
|
1203
|
+
sourceEl: draggable,
|
|
1204
|
+
handleEl: handle,
|
|
1205
|
+
originDrop,
|
|
1206
|
+
isDragScope: Boolean(this.#adapter.closestDragScope(draggable)),
|
|
1207
|
+
isCopy: Boolean(this.#adapter.getAttr(draggable, ATTR.copyAttr)),
|
|
1208
|
+
mirrorOffsetX: event.clientX - rect.left,
|
|
1209
|
+
mirrorOffsetY: event.clientY - rect.top
|
|
1210
|
+
})?.accepted) return { accepted: false };
|
|
1211
|
+
const monitor = this.#state.session;
|
|
1212
|
+
monitor.threshold = this.#config.threshold;
|
|
1213
|
+
monitor.adapter = this.#adapter;
|
|
1214
|
+
this.#frame.reset(performance.now());
|
|
1215
|
+
this.#adapter.beginFrame();
|
|
1216
|
+
monitor.cache.frameId = this.#adapter.frameId;
|
|
1217
|
+
const ctx = createContext({
|
|
1218
|
+
dnd: this,
|
|
1219
|
+
session: monitor,
|
|
1220
|
+
store: this.#store,
|
|
1221
|
+
frame: this.#frame,
|
|
1222
|
+
adapter: this.#adapter
|
|
1223
|
+
});
|
|
1224
|
+
this.#runtime.onDown(ctx, event);
|
|
1225
|
+
this.#frame.commit();
|
|
1226
|
+
this.#adapter.endFrame();
|
|
1227
|
+
return {
|
|
1228
|
+
accepted: true,
|
|
1229
|
+
pointerId: event.pointerId,
|
|
1230
|
+
signal: monitor.dragAbort.signal
|
|
1231
|
+
};
|
|
1232
|
+
};
|
|
1233
|
+
/**
|
|
1234
|
+
* pointermove:
|
|
1235
|
+
* - dispatch MOVE 到状态机(更新坐标、started/dirty 等)
|
|
1236
|
+
* - startedNow:触发 onStart + emit dragstart
|
|
1237
|
+
* - started:请求 rAF 进入 per-frame 驱动
|
|
1238
|
+
*/
|
|
1239
|
+
#onPointerMove = ({ pointerId, x, y }) => {
|
|
1240
|
+
const effects = this.#state.dispatch({
|
|
1241
|
+
type: "MOVE",
|
|
1242
|
+
pointerId,
|
|
1243
|
+
x,
|
|
1244
|
+
y
|
|
1245
|
+
});
|
|
1246
|
+
if (!effects?.handled) return;
|
|
1247
|
+
const monitor = this.#state.session;
|
|
1248
|
+
if (effects.startedNow) {
|
|
1249
|
+
const ctx = createContext({
|
|
1250
|
+
dnd: this,
|
|
1251
|
+
session: monitor,
|
|
1252
|
+
store: this.#store,
|
|
1253
|
+
frame: this.#frame,
|
|
1254
|
+
adapter: this.#adapter
|
|
1255
|
+
});
|
|
1256
|
+
this.#runtime.onStart(ctx);
|
|
1257
|
+
this.emit("dragstart", createPayload("dragstart", monitor));
|
|
1258
|
+
}
|
|
1259
|
+
if (effects.started) this.#scheduler.request();
|
|
1260
|
+
};
|
|
1261
|
+
/**
|
|
1262
|
+
* pointerup / pointercancel / blur:
|
|
1263
|
+
* - dispatch END 到状态机
|
|
1264
|
+
* - 确保 drop 命中信息是最新的(避免最后一帧没跑)
|
|
1265
|
+
* - 插件 onEnd 清理
|
|
1266
|
+
* - 发出 drop/cancel 事件
|
|
1267
|
+
* - abort 会话信号 & 重置 session
|
|
1268
|
+
*/
|
|
1269
|
+
#onPointerUp = ({ event, pointerId, reason }) => {
|
|
1270
|
+
const effects = this.#state.dispatch({
|
|
1271
|
+
type: "END",
|
|
1272
|
+
pointerId,
|
|
1273
|
+
reason
|
|
1274
|
+
});
|
|
1275
|
+
if (!effects?.handled) return;
|
|
1276
|
+
const monitor = this.#state.session;
|
|
1277
|
+
if ("pointerId" in event) event.target?.releasePointerCapture?.(event.pointerId);
|
|
1278
|
+
const ended = Boolean(effects.ended);
|
|
1279
|
+
this.#adapter.beginFrame();
|
|
1280
|
+
monitor.cache.frameId = this.#adapter.frameId;
|
|
1281
|
+
monitor.currentDrop = this.#adapter.hitDropForSource(monitor.x, monitor.y, monitor.namespace, monitor.sourceEl);
|
|
1282
|
+
monitor.currentDropRect = monitor.currentDrop ? this.#adapter.measureRect(monitor.currentDrop) : void 0;
|
|
1283
|
+
this.#adapter.endFrame();
|
|
1284
|
+
const ctx = createContext({
|
|
1285
|
+
dnd: this,
|
|
1286
|
+
session: monitor,
|
|
1287
|
+
store: this.#store,
|
|
1288
|
+
frame: this.#frame,
|
|
1289
|
+
adapter: this.#adapter
|
|
1290
|
+
});
|
|
1291
|
+
this.#runtime.onEnd(ctx, {
|
|
1292
|
+
ended,
|
|
1293
|
+
reason: reason || "pointerup"
|
|
1294
|
+
});
|
|
1295
|
+
const payload = createPayload(ended ? "drop" : "cancel", monitor);
|
|
1296
|
+
monitor.dragAbort?.abort();
|
|
1297
|
+
monitor.dragAbort = void 0;
|
|
1298
|
+
this.#scheduler.cancel();
|
|
1299
|
+
this.#frameCtx = void 0;
|
|
1300
|
+
const next = this.#state.resetSession({ ended });
|
|
1301
|
+
next.adapter = this.#adapter;
|
|
1302
|
+
ended ? this.emit("drop", payload) : this.emit("cancel", payload);
|
|
1303
|
+
event.preventDefault?.();
|
|
1304
|
+
};
|
|
1305
|
+
/**
|
|
1306
|
+
* 单帧 rAF 回调:
|
|
1307
|
+
* - started 且 active 才会运行
|
|
1308
|
+
* - 若 monitor.dirty:执行 measure/compute/commit,并 emit drag
|
|
1309
|
+
* - afterDrag 钩子用于处理滚动/惯性等副作用
|
|
1310
|
+
* - 返回 true 表示下一帧继续
|
|
1311
|
+
*/
|
|
1312
|
+
#onFrame = (now) => {
|
|
1313
|
+
const monitor = this.#state.session;
|
|
1314
|
+
if (!monitor.active || !monitor.started) return false;
|
|
1315
|
+
this.#frame.reset(now);
|
|
1316
|
+
this.#adapter.beginFrame();
|
|
1317
|
+
monitor.cache.frameId = this.#adapter.frameId;
|
|
1318
|
+
let ctx = this.#frameCtx;
|
|
1319
|
+
if (!ctx || ctx.session !== monitor) ctx = this.#frameCtx = createContext({
|
|
1320
|
+
dnd: this,
|
|
1321
|
+
session: monitor,
|
|
1322
|
+
store: this.#store,
|
|
1323
|
+
frame: this.#frame,
|
|
1324
|
+
adapter: this.#adapter
|
|
1325
|
+
});
|
|
1326
|
+
else ctx.resetPayloadCache();
|
|
1327
|
+
let scrolled = false;
|
|
1328
|
+
if (monitor.dirty) {
|
|
1329
|
+
this.#runtime.onMeasure(ctx);
|
|
1330
|
+
this.#runtime.onCompute(ctx);
|
|
1331
|
+
this.emit("drag", ctx.payload("drag"));
|
|
1332
|
+
this.#runtime.onCommit(ctx);
|
|
1333
|
+
this.#frame.commit();
|
|
1334
|
+
if (this.#frame.scrolled) scrolled = true;
|
|
1335
|
+
monitor.dirty = false;
|
|
1336
|
+
}
|
|
1337
|
+
if (this.#runtime.onAfterDrag(ctx)) scrolled = true;
|
|
1338
|
+
if (scrolled) monitor.dirty = true;
|
|
1339
|
+
this.#adapter.endFrame();
|
|
1340
|
+
return monitor.dirty || scrolled;
|
|
1341
|
+
};
|
|
1342
|
+
};
|
|
1343
|
+
//#endregion
|
|
1344
|
+
//#region src/services/DropService.js
|
|
1345
|
+
/**
|
|
1346
|
+
* DropService
|
|
1347
|
+
*
|
|
1348
|
+
* 目标:
|
|
1349
|
+
* - 在每一帧(onMeasure 阶段)保证 drop 命中信息的”单一事实来源”(single source of truth)
|
|
1350
|
+
* - 统一维护以下字段,避免多处重复计算/不一致:
|
|
1351
|
+
* - monitor.currentDrop:当前命中的 drop 元素
|
|
1352
|
+
* - monitor.currentDropRect:当前 drop 的测量矩形(DOMRect/自定义 rect)
|
|
1353
|
+
* - monitor.currentAllowed:当前是否允许 drop(权威数据源)
|
|
1354
|
+
* - 统一维护 canDrop/视觉反馈:
|
|
1355
|
+
* - 给 currentDrop 添加/移除 CLASS_NAMES.canDrop / CLASS_NAMES.noDrop
|
|
1356
|
+
*
|
|
1357
|
+
* 注意:
|
|
1358
|
+
* - order = 10,确保在其他依赖 drop 状态的服务(如 DropIndicatorService)之前执行
|
|
1359
|
+
* - currentAllowed 由本服务在 onCompute 阶段计算,其他服务应直接使用该值
|
|
1360
|
+
*/
|
|
1361
|
+
var DropService = class {
|
|
1362
|
+
order = 10;
|
|
1363
|
+
/**
|
|
1364
|
+
* onMeasure:
|
|
1365
|
+
* - 只做 DOM 读取/测量相关工作(命中测试、measureRect)
|
|
1366
|
+
* - 负责在每帧更新 currentDrop / currentDropRect
|
|
1367
|
+
*/
|
|
1368
|
+
onMeasure(ctx) {
|
|
1369
|
+
const m = ctx.session;
|
|
1370
|
+
if (!m.active || !m.started) return;
|
|
1371
|
+
const drop = ctx.adapter.hitDropForSource(m.x, m.y, m.namespace, m.sourceEl);
|
|
1372
|
+
if (drop !== m.currentDrop) {
|
|
1373
|
+
m.currentDrop = drop;
|
|
1374
|
+
if (m.cache) m.cache.dropRectDirty = true;
|
|
1375
|
+
}
|
|
1376
|
+
if (!drop) {
|
|
1377
|
+
m.currentDropRect = void 0;
|
|
1378
|
+
if (m.cache) m.cache.dropRectDirty = false;
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
if (m.cache?.dropRectDirty || !m.currentDropRect) {
|
|
1382
|
+
m.currentDropRect = ctx.adapter.measureRect(drop);
|
|
1383
|
+
m.cache.dropRectDirty = false;
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
/**
|
|
1387
|
+
* onCompute:
|
|
1388
|
+
* - 计算当前 drop 是否允许(dnd.canDrop(payload))
|
|
1389
|
+
* - 只写 session,不写 DOM
|
|
1390
|
+
* - 这是 currentAllowed 的权威计算点,其他服务应直接使用该值
|
|
1391
|
+
*/
|
|
1392
|
+
onCompute(ctx) {
|
|
1393
|
+
const m = ctx.session;
|
|
1394
|
+
if (!m.active || !m.started) return;
|
|
1395
|
+
if (!m.currentDrop) {
|
|
1396
|
+
m.currentAllowed = false;
|
|
1397
|
+
return;
|
|
1398
|
+
}
|
|
1399
|
+
const fn = ctx.dnd?.canDrop;
|
|
1400
|
+
if (typeof fn !== "function") {
|
|
1401
|
+
m.currentAllowed = true;
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
try {
|
|
1405
|
+
m.currentAllowed = !!fn(ctx.payload("drag"));
|
|
1406
|
+
} catch {
|
|
1407
|
+
m.currentAllowed = false;
|
|
1408
|
+
}
|
|
1409
|
+
}
|
|
1410
|
+
/**
|
|
1411
|
+
* onCommit:
|
|
1412
|
+
* - 根据 currentDrop/currentAllowed 写入 drop 的 class
|
|
1413
|
+
*/
|
|
1414
|
+
onCommit(ctx) {
|
|
1415
|
+
const m = ctx.session;
|
|
1416
|
+
if (!m?.active || !m?.started) return;
|
|
1417
|
+
const cache = m.cache || (m.cache = {});
|
|
1418
|
+
const prev = cache._dropClassEl;
|
|
1419
|
+
const cur = m.currentDrop;
|
|
1420
|
+
if (isHTMLElement(prev) && prev !== cur) {
|
|
1421
|
+
ctx.frame.toggleClass(prev, CLASS_NAMES.canDrop, false);
|
|
1422
|
+
ctx.frame.toggleClass(prev, CLASS_NAMES.noDrop, false);
|
|
1423
|
+
}
|
|
1424
|
+
if (isHTMLElement(cur)) {
|
|
1425
|
+
ctx.frame.toggleClass(cur, CLASS_NAMES.canDrop, !!m.currentAllowed);
|
|
1426
|
+
ctx.frame.toggleClass(cur, CLASS_NAMES.noDrop, !m.currentAllowed);
|
|
1427
|
+
}
|
|
1428
|
+
cache._dropClassEl = cur;
|
|
1429
|
+
}
|
|
1430
|
+
onEnd(ctx) {
|
|
1431
|
+
const m = ctx?.session;
|
|
1432
|
+
if (!m) return;
|
|
1433
|
+
const prev = m.cache?._dropClassEl;
|
|
1434
|
+
if (isHTMLElement(prev)) prev.classList.remove(CLASS_NAMES.canDrop, CLASS_NAMES.noDrop);
|
|
1435
|
+
if (m.cache) m.cache._dropClassEl = void 0;
|
|
1436
|
+
}
|
|
1437
|
+
onDestroy(_dnd, session) {
|
|
1438
|
+
this.onEnd({ session });
|
|
1439
|
+
}
|
|
1440
|
+
};
|
|
1441
|
+
//#endregion
|
|
1442
|
+
//#region src/services/MirrorService.js
|
|
1443
|
+
var MirrorService = class {
|
|
1444
|
+
order = 0;
|
|
1445
|
+
onStart = (ctx) => {
|
|
1446
|
+
const s = ctx?.session;
|
|
1447
|
+
if (!s?.sourceEl || s.mirrorEl) return;
|
|
1448
|
+
s.sourceEl.classList.add(CLASS_NAMES.dragging);
|
|
1449
|
+
if (s.sourceEl.hasAttribute?.(ATTR.ignoreMirrorAttr)) return;
|
|
1450
|
+
const rect = ctx.adapter.measureRect(s.sourceEl);
|
|
1451
|
+
const m = typeof ctx?.dnd?.renderMirror === "function" ? ctx.dnd.renderMirror(ctx) : s.sourceEl.cloneNode(true);
|
|
1452
|
+
if (!(m instanceof HTMLElement)) return;
|
|
1453
|
+
m.setAttribute(ATTR.ignoreMirrorAttr, "");
|
|
1454
|
+
m.classList.add(CLASS_NAMES.mirror);
|
|
1455
|
+
Object.assign(m.style, {
|
|
1456
|
+
width: `${rect.width}px`,
|
|
1457
|
+
height: `${rect.height}px`,
|
|
1458
|
+
position: "fixed"
|
|
1459
|
+
});
|
|
1460
|
+
(ctx.dnd?.root || document.body).appendChild(m);
|
|
1461
|
+
s.mirrorEl = m;
|
|
1462
|
+
};
|
|
1463
|
+
onCommit = (ctx) => {
|
|
1464
|
+
const s = ctx?.session;
|
|
1465
|
+
if (!s?.active || !s.started) return;
|
|
1466
|
+
if (s.mirrorEl) {
|
|
1467
|
+
const x = s.x - (s.mirrorOffsetX || 0);
|
|
1468
|
+
const y = s.y - (s.mirrorOffsetY || 0);
|
|
1469
|
+
ctx.frame.setStyle(s.mirrorEl, "transform", `translate3d(${x}px, ${y}px, 0)`);
|
|
1470
|
+
}
|
|
1471
|
+
};
|
|
1472
|
+
onEnd = (ctx) => {
|
|
1473
|
+
const s = ctx?.session;
|
|
1474
|
+
if (!s) return;
|
|
1475
|
+
if (s.mirrorEl) {
|
|
1476
|
+
s.mirrorEl.remove();
|
|
1477
|
+
s.mirrorEl = void 0;
|
|
1478
|
+
}
|
|
1479
|
+
s.sourceEl?.classList.remove(CLASS_NAMES.dragging);
|
|
1480
|
+
};
|
|
1481
|
+
onDestroy = (_dnd, session) => {
|
|
1482
|
+
this.onEnd({ session });
|
|
1483
|
+
};
|
|
1484
|
+
};
|
|
1485
|
+
//#endregion
|
|
1486
|
+
//#region src/services/AutoScrollService.js
|
|
1487
|
+
var OVERFLOW_RE = /(auto|scroll|overlay)/i;
|
|
1488
|
+
function isScrollable(el) {
|
|
1489
|
+
if (!(el instanceof HTMLElement)) return false;
|
|
1490
|
+
const s = getComputedStyle(el);
|
|
1491
|
+
const oy = OVERFLOW_RE.test(s.overflowY) && el.scrollHeight > el.clientHeight + 1;
|
|
1492
|
+
return OVERFLOW_RE.test(s.overflowX) && el.scrollWidth > el.clientWidth + 1 || oy;
|
|
1493
|
+
}
|
|
1494
|
+
function findScrollTarget(fromEl) {
|
|
1495
|
+
let el = fromEl;
|
|
1496
|
+
while (el) {
|
|
1497
|
+
if (isScrollable(el)) return el;
|
|
1498
|
+
el = el.parentElement;
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
function edgePlan(p, start, end, edge) {
|
|
1502
|
+
if (p < start + edge) return {
|
|
1503
|
+
dir: -1,
|
|
1504
|
+
k: (start + edge - p) / edge
|
|
1505
|
+
};
|
|
1506
|
+
if (p > end - edge) return {
|
|
1507
|
+
dir: 1,
|
|
1508
|
+
k: (p - (end - edge)) / edge
|
|
1509
|
+
};
|
|
1510
|
+
return {
|
|
1511
|
+
dir: 0,
|
|
1512
|
+
k: 0
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
var AutoScrollService = class {
|
|
1516
|
+
order = 1e4;
|
|
1517
|
+
constructor(opts = {}) {
|
|
1518
|
+
this.edge = opts.edge ?? AUTO_SCROLL.EDGE;
|
|
1519
|
+
this.minSpeed = opts.minSpeed ?? AUTO_SCROLL.MIN_SPEED;
|
|
1520
|
+
this.maxSpeed = opts.maxSpeed ?? AUTO_SCROLL.MAX_SPEED;
|
|
1521
|
+
this.allowWindowScroll = opts.allowWindowScroll ?? true;
|
|
1522
|
+
}
|
|
1523
|
+
onAfterDrag = (ctx) => {
|
|
1524
|
+
const s = ctx.session;
|
|
1525
|
+
if (!s.active || !s.started) return false;
|
|
1526
|
+
const cache = s.cache;
|
|
1527
|
+
let targetEl;
|
|
1528
|
+
let useWindow;
|
|
1529
|
+
if (cache && cache.autoScrollTargetDrop === s.currentDrop) {
|
|
1530
|
+
targetEl = cache.autoScrollTarget;
|
|
1531
|
+
useWindow = cache.autoScrollUseWindow;
|
|
1532
|
+
} else {
|
|
1533
|
+
targetEl = s.currentDrop ? findScrollTarget(s.currentDrop) : void 0;
|
|
1534
|
+
useWindow = false;
|
|
1535
|
+
if (!targetEl && this.allowWindowScroll) {
|
|
1536
|
+
targetEl = document.scrollingElement || document.documentElement;
|
|
1537
|
+
useWindow = !!targetEl;
|
|
1538
|
+
}
|
|
1539
|
+
if (cache) {
|
|
1540
|
+
cache.autoScrollTargetDrop = s.currentDrop;
|
|
1541
|
+
cache.autoScrollTarget = targetEl;
|
|
1542
|
+
cache.autoScrollUseWindow = useWindow;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (!targetEl) return false;
|
|
1546
|
+
const now = ctx.frame.now;
|
|
1547
|
+
const last = s.cache.autoScrollLastTs || 0;
|
|
1548
|
+
const dt = last ? Math.max(AUTO_SCROLL.MIN_DT, Math.min(AUTO_SCROLL.MAX_DT, now - last)) : AUTO_SCROLL.DEFAULT_DT;
|
|
1549
|
+
if (s.cache) s.cache.autoScrollLastTs = now;
|
|
1550
|
+
const rect = useWindow ? {
|
|
1551
|
+
left: 0,
|
|
1552
|
+
top: 0,
|
|
1553
|
+
right: innerWidth,
|
|
1554
|
+
bottom: innerHeight
|
|
1555
|
+
} : ctx.adapter.measureRect(targetEl);
|
|
1556
|
+
const px = s.x, py = s.y;
|
|
1557
|
+
const ex = edgePlan(px, rect.left, rect.right, this.edge);
|
|
1558
|
+
const ey = edgePlan(py, rect.top, rect.bottom, this.edge);
|
|
1559
|
+
const speed = (k) => this.minSpeed + (this.maxSpeed - this.minSpeed) * k;
|
|
1560
|
+
const dx = ex.dir ? ex.dir * speed(ex.k) * dt / 1e3 : 0;
|
|
1561
|
+
const dy = ey.dir ? ey.dir * speed(ey.k) * dt / 1e3 : 0;
|
|
1562
|
+
if (!dx && !dy) return false;
|
|
1563
|
+
const beforeTop = targetEl.scrollTop, beforeLeft = targetEl.scrollLeft;
|
|
1564
|
+
if (useWindow) window.scrollBy(dx, dy);
|
|
1565
|
+
else targetEl.scrollBy({
|
|
1566
|
+
left: dx,
|
|
1567
|
+
top: dy,
|
|
1568
|
+
behavior: "auto"
|
|
1569
|
+
});
|
|
1570
|
+
const scrolled = beforeTop !== targetEl.scrollTop || beforeLeft !== targetEl.scrollLeft;
|
|
1571
|
+
if (scrolled && ctx.frame) ctx.frame.scrolled = true;
|
|
1572
|
+
return scrolled;
|
|
1573
|
+
};
|
|
1574
|
+
onEnd = (ctx) => {
|
|
1575
|
+
const s = ctx?.session;
|
|
1576
|
+
if (s?.cache) {
|
|
1577
|
+
s.cache.autoScrollLastTs = 0;
|
|
1578
|
+
s.cache.autoScrollTargetDrop = void 0;
|
|
1579
|
+
s.cache.autoScrollTarget = void 0;
|
|
1580
|
+
s.cache.autoScrollUseWindow = false;
|
|
1581
|
+
}
|
|
1582
|
+
};
|
|
1583
|
+
};
|
|
1584
|
+
//#endregion
|
|
1585
|
+
//#region src/services/DropIndicatorService.js
|
|
1586
|
+
var REGION_CLS = {
|
|
1587
|
+
top: CLASS_NAMES.indicatorTop,
|
|
1588
|
+
right: CLASS_NAMES.indicatorRight,
|
|
1589
|
+
bottom: CLASS_NAMES.indicatorBottom,
|
|
1590
|
+
left: CLASS_NAMES.indicatorLeft
|
|
1591
|
+
};
|
|
1592
|
+
var REGION_LIST = Object.values(REGION_CLS);
|
|
1593
|
+
var VALID = /* @__PURE__ */ new Set([
|
|
1594
|
+
"top",
|
|
1595
|
+
"right",
|
|
1596
|
+
"bottom",
|
|
1597
|
+
"left"
|
|
1598
|
+
]);
|
|
1599
|
+
function toggle(ctx, el, cls, on) {
|
|
1600
|
+
if (!isHTMLElement(el)) return;
|
|
1601
|
+
ctx.frame.toggleClass(el, cls, !!on);
|
|
1602
|
+
}
|
|
1603
|
+
function parseSupport(dropEl) {
|
|
1604
|
+
if (!isHTMLElement(dropEl)) return;
|
|
1605
|
+
const name = ATTR.dropIndicatorAttr || "drop-indicator";
|
|
1606
|
+
const dataName = `data-${name}`;
|
|
1607
|
+
let raw = void 0;
|
|
1608
|
+
if (dropEl.hasAttribute?.(name)) raw = dropEl.getAttribute?.(name);
|
|
1609
|
+
else if (dropEl.hasAttribute?.(dataName)) raw = dropEl.getAttribute?.(dataName);
|
|
1610
|
+
else return;
|
|
1611
|
+
const text = (raw ?? "").trim();
|
|
1612
|
+
if (!text) return "all";
|
|
1613
|
+
const set = /* @__PURE__ */ new Set();
|
|
1614
|
+
for (const p of text.split(/[,\s]+/)) {
|
|
1615
|
+
const k = (p || "").trim().toLowerCase();
|
|
1616
|
+
if (VALID.has(k)) set.add(k);
|
|
1617
|
+
}
|
|
1618
|
+
return set.size ? set : void 0;
|
|
1619
|
+
}
|
|
1620
|
+
function getSupport(ctx, dropEl) {
|
|
1621
|
+
const cache = ctx.session?.cache;
|
|
1622
|
+
if (!cache) return parseSupport(dropEl);
|
|
1623
|
+
if (cache.indicatorSupportEl === dropEl) return cache.indicatorSupport;
|
|
1624
|
+
const support = parseSupport(dropEl);
|
|
1625
|
+
cache.indicatorSupportEl = dropEl;
|
|
1626
|
+
cache.indicatorSupport = support;
|
|
1627
|
+
return support;
|
|
1628
|
+
}
|
|
1629
|
+
function ensureIndicator(ctx) {
|
|
1630
|
+
const m = ctx.session;
|
|
1631
|
+
const doc = ctx.dnd?.root?.ownerDocument || document;
|
|
1632
|
+
let el = m.indicatorEl;
|
|
1633
|
+
if (!isHTMLElement(el)) {
|
|
1634
|
+
el = doc.createElement("div");
|
|
1635
|
+
el.classList.add(CLASS_NAMES.dropIndicator);
|
|
1636
|
+
m.indicatorEl = el;
|
|
1637
|
+
}
|
|
1638
|
+
const parent = m.currentDrop || ctx.dnd?.root || doc.body;
|
|
1639
|
+
if (isHTMLElement(parent) && el.parentNode !== parent) parent.appendChild(el);
|
|
1640
|
+
return el;
|
|
1641
|
+
}
|
|
1642
|
+
function computeRegion(m, support) {
|
|
1643
|
+
if (!m.currentDrop || !m.currentDropRect) return;
|
|
1644
|
+
const f = computeDropRegion(m, support);
|
|
1645
|
+
if (f.isOverLeft()) return "left";
|
|
1646
|
+
if (f.isOverRight()) return "right";
|
|
1647
|
+
if (f.isOverTop()) return "top";
|
|
1648
|
+
if (f.isOverBottom()) return "bottom";
|
|
1649
|
+
}
|
|
1650
|
+
/**
|
|
1651
|
+
* 获取当前 drop 是否允许
|
|
1652
|
+
* 优先使用 DropService 已计算的 currentAllowed
|
|
1653
|
+
* 这样避免重复计算,保持单一数据源
|
|
1654
|
+
*/
|
|
1655
|
+
function resolveAllowed(ctx) {
|
|
1656
|
+
const m = ctx.session;
|
|
1657
|
+
if (!m.currentDrop) return false;
|
|
1658
|
+
if (typeof m.currentAllowed === "boolean") return m.currentAllowed;
|
|
1659
|
+
return true;
|
|
1660
|
+
}
|
|
1661
|
+
var DropIndicatorService = class {
|
|
1662
|
+
order = 40;
|
|
1663
|
+
onCommit(ctx) {
|
|
1664
|
+
const m = ctx.session;
|
|
1665
|
+
if (!m?.active || !m?.started) return;
|
|
1666
|
+
const drop = m.currentDrop;
|
|
1667
|
+
const support = getSupport(ctx, drop);
|
|
1668
|
+
if (!support) {
|
|
1669
|
+
const ind = m.indicatorEl;
|
|
1670
|
+
if (isHTMLElement(ind)) {
|
|
1671
|
+
toggle(ctx, ind, CLASS_NAMES.dropIndicatorActive, false);
|
|
1672
|
+
for (const c of REGION_LIST) toggle(ctx, ind, c, false);
|
|
1673
|
+
}
|
|
1674
|
+
m.indicatorRegion = void 0;
|
|
1675
|
+
return;
|
|
1676
|
+
}
|
|
1677
|
+
const ind = ensureIndicator(ctx);
|
|
1678
|
+
const allowed = resolveAllowed(ctx);
|
|
1679
|
+
const region = computeRegion(m, support);
|
|
1680
|
+
const okRegion = support === "all" ? region : region && support.has(region) ? region : void 0;
|
|
1681
|
+
const show = !!(drop && m.currentDropRect && allowed && okRegion);
|
|
1682
|
+
toggle(ctx, ind, CLASS_NAMES.dropIndicatorActive, show);
|
|
1683
|
+
m.indicatorRegion = show ? okRegion : void 0;
|
|
1684
|
+
if (!show) for (const c of REGION_LIST) toggle(ctx, ind, c, false);
|
|
1685
|
+
else for (const [r, c] of Object.entries(REGION_CLS)) toggle(ctx, ind, c, r === okRegion);
|
|
1686
|
+
}
|
|
1687
|
+
onEnd(ctx) {
|
|
1688
|
+
const m = ctx.session;
|
|
1689
|
+
if (!m) return;
|
|
1690
|
+
m.lastIndicatorRegion = m.indicatorRegion ?? "inside";
|
|
1691
|
+
if (isHTMLElement(m.indicatorEl)) m.indicatorEl.remove();
|
|
1692
|
+
m.indicatorEl = void 0;
|
|
1693
|
+
m.indicatorRegion = void 0;
|
|
1694
|
+
}
|
|
1695
|
+
onDestroy(_dnd, session) {
|
|
1696
|
+
this.onEnd({ session });
|
|
1697
|
+
}
|
|
1698
|
+
};
|
|
1699
|
+
//#endregion
|
|
1700
|
+
//#region src/services/ActiveSelectionService.js
|
|
1701
|
+
var ACTIVE_WRAP_CLASS = "draggable-dot-wrap";
|
|
1702
|
+
var DOT_CLASS = "draggable-dot";
|
|
1703
|
+
var ROTATE_CLASS = "draggable-rotate";
|
|
1704
|
+
function buildWrap(doc, opt = {}) {
|
|
1705
|
+
const { resizable = true, rotatable = true } = opt;
|
|
1706
|
+
const wrap = doc.createElement("div");
|
|
1707
|
+
wrap.classList.add(ACTIVE_WRAP_CLASS);
|
|
1708
|
+
wrap.dataset.resizable = String(resizable);
|
|
1709
|
+
wrap.dataset.rotatable = String(rotatable);
|
|
1710
|
+
if (resizable) for (const [dir, pos] of [
|
|
1711
|
+
["nw", "tl"],
|
|
1712
|
+
["n", "tm"],
|
|
1713
|
+
["ne", "tr"],
|
|
1714
|
+
["e", "rm"],
|
|
1715
|
+
["se", "br"],
|
|
1716
|
+
["s", "bm"],
|
|
1717
|
+
["sw", "bl"],
|
|
1718
|
+
["w", "lm"]
|
|
1719
|
+
]) {
|
|
1720
|
+
const dot = doc.createElement("div");
|
|
1721
|
+
dot.classList.add(DOT_CLASS);
|
|
1722
|
+
dot.setAttribute(ATTR.handleResizeAttr, "");
|
|
1723
|
+
dot.dataset.dir = dir;
|
|
1724
|
+
dot.dataset.pos = pos;
|
|
1725
|
+
wrap.appendChild(dot);
|
|
1726
|
+
}
|
|
1727
|
+
if (rotatable) {
|
|
1728
|
+
const rotate = doc.createElement("div");
|
|
1729
|
+
rotate.classList.add(ROTATE_CLASS);
|
|
1730
|
+
rotate.setAttribute(ATTR.handleRotateAttr, "");
|
|
1731
|
+
rotate.dataset.dir = "rotate";
|
|
1732
|
+
wrap.appendChild(rotate);
|
|
1733
|
+
}
|
|
1734
|
+
return wrap;
|
|
1735
|
+
}
|
|
1736
|
+
function removeWrap(el) {
|
|
1737
|
+
if (!isHTMLElement(el)) return;
|
|
1738
|
+
el.querySelector?.(`.${ACTIVE_WRAP_CLASS}`)?.remove();
|
|
1739
|
+
}
|
|
1740
|
+
function ensureWrapIfScoped(el) {
|
|
1741
|
+
if (!isHTMLElement(el)) return;
|
|
1742
|
+
const scoped = !!el.closest?.(SCOPE_SEL);
|
|
1743
|
+
let existedWrap = el.querySelector?.(`.draggable-dot-wrap`) || null;
|
|
1744
|
+
let hasWrap = !!existedWrap;
|
|
1745
|
+
const resizable = isToggleEnabled(el, ATTR.resizableAttr, true);
|
|
1746
|
+
const rotatable = isToggleEnabled(el, ATTR.rotatableAttr, true);
|
|
1747
|
+
const needWrap = resizable || rotatable;
|
|
1748
|
+
if (!scoped) {
|
|
1749
|
+
if (hasWrap) removeWrap(el);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
if (!needWrap) {
|
|
1753
|
+
if (hasWrap) removeWrap(el);
|
|
1754
|
+
return;
|
|
1755
|
+
}
|
|
1756
|
+
if (!!existedWrap && (String(existedWrap.dataset?.resizable) !== String(resizable) || String(existedWrap.dataset?.rotatable) !== String(rotatable))) {
|
|
1757
|
+
existedWrap.remove();
|
|
1758
|
+
existedWrap = null;
|
|
1759
|
+
hasWrap = false;
|
|
1760
|
+
}
|
|
1761
|
+
if (!hasWrap) {
|
|
1762
|
+
const pos = getComputedStyle(el).position;
|
|
1763
|
+
if (pos === "static" || !pos) el.style.position = "relative";
|
|
1764
|
+
el.appendChild(buildWrap(el.ownerDocument || document, {
|
|
1765
|
+
resizable,
|
|
1766
|
+
rotatable
|
|
1767
|
+
}));
|
|
1768
|
+
}
|
|
1769
|
+
}
|
|
1770
|
+
/**
|
|
1771
|
+
* 清理一个选中值(可能是数组[多选]或单个元素):移除 active class + wrap。
|
|
1772
|
+
* toggleFn 可选:若传入则走帧队列(ctx.frame.toggleClass),否则直接 classList。
|
|
1773
|
+
*/
|
|
1774
|
+
function clearSelectionValue(value, toggleFn) {
|
|
1775
|
+
if (!value) return;
|
|
1776
|
+
const els = Array.isArray(value) ? value : [value];
|
|
1777
|
+
for (const el of els) {
|
|
1778
|
+
if (toggleFn) toggleFn(el, CLASS_NAMES.active, false);
|
|
1779
|
+
else el?.classList?.remove?.(CLASS_NAMES.active);
|
|
1780
|
+
removeWrap(el);
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
function setActiveWithWrap(ctx, map, dragEl, ns) {
|
|
1784
|
+
const prev = map.get(ns);
|
|
1785
|
+
if (prev && prev !== dragEl) clearSelectionValue(prev);
|
|
1786
|
+
dragEl.classList.add(CLASS_NAMES.active);
|
|
1787
|
+
ensureWrapIfScoped(dragEl);
|
|
1788
|
+
map.set(ns, dragEl);
|
|
1789
|
+
}
|
|
1790
|
+
function setMultipleActive(map, elements, ns) {
|
|
1791
|
+
clearSelectionValue(map.get(ns));
|
|
1792
|
+
for (const el of elements) {
|
|
1793
|
+
el.classList.add(CLASS_NAMES.active);
|
|
1794
|
+
ensureWrapIfScoped(el);
|
|
1795
|
+
}
|
|
1796
|
+
map.set(ns, elements);
|
|
1797
|
+
}
|
|
1798
|
+
function clearAll(map, namespace) {
|
|
1799
|
+
if (namespace !== void 0) {
|
|
1800
|
+
const val = map.get(namespace);
|
|
1801
|
+
if (val) {
|
|
1802
|
+
clearSelectionValue(val);
|
|
1803
|
+
map.delete(namespace);
|
|
1804
|
+
}
|
|
1805
|
+
return;
|
|
1806
|
+
}
|
|
1807
|
+
for (const val of map.values()) clearSelectionValue(val);
|
|
1808
|
+
map.clear();
|
|
1809
|
+
}
|
|
1810
|
+
var ActiveSelectionService = class {
|
|
1811
|
+
order = -50;
|
|
1812
|
+
#dnd;
|
|
1813
|
+
onAttach(dnd) {
|
|
1814
|
+
this.#dnd = dnd;
|
|
1815
|
+
}
|
|
1816
|
+
onRootChange(nextRoot, _prevRoot, signal) {
|
|
1817
|
+
if (!isHTMLElement(nextRoot)) return;
|
|
1818
|
+
const monitor = this.#dnd.monitor;
|
|
1819
|
+
nextRoot.addEventListener("pointerdown", (e) => {
|
|
1820
|
+
if (e.button !== 0) return;
|
|
1821
|
+
if (isIgnoreClick(e.target)) return;
|
|
1822
|
+
const map = monitor.selectedByNs;
|
|
1823
|
+
if (!map) return;
|
|
1824
|
+
const drag = monitor.adapter.closestDraggable(e.target);
|
|
1825
|
+
if (!drag) {
|
|
1826
|
+
clearAll(map);
|
|
1827
|
+
return;
|
|
1828
|
+
}
|
|
1829
|
+
const ns = monitor.adapter.getNamespace(drag);
|
|
1830
|
+
const prev = map.get(ns);
|
|
1831
|
+
if (Array.isArray(prev) && prev.length > 1 && prev.includes(drag)) return;
|
|
1832
|
+
setActiveWithWrap(null, map, drag, ns);
|
|
1833
|
+
}, {
|
|
1834
|
+
capture: true,
|
|
1835
|
+
signal
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
onDown(ctx, event) {
|
|
1839
|
+
const m = ctx?.session;
|
|
1840
|
+
if (!m?.sourceEl || event?.button !== 0) return;
|
|
1841
|
+
if (isIgnoreClick(event.target)) return;
|
|
1842
|
+
const map = ctx.store?.selectedByNs || m.selectedByNs;
|
|
1843
|
+
if (!map) return;
|
|
1844
|
+
const ns = m.namespace || ctx.adapter.getNamespace(m.sourceEl);
|
|
1845
|
+
const prev = map.get(ns);
|
|
1846
|
+
if (Array.isArray(prev) && prev.length > 1 && prev.includes(m.sourceEl)) return;
|
|
1847
|
+
if (prev && prev !== m.sourceEl) clearSelectionValue(prev, (el, cls, on) => ctx.frame.toggleClass(el, cls, on));
|
|
1848
|
+
ctx.frame.toggleClass(m.sourceEl, CLASS_NAMES.active, true);
|
|
1849
|
+
ensureWrapIfScoped(m.sourceEl);
|
|
1850
|
+
map.set(ns, m.sourceEl);
|
|
1851
|
+
}
|
|
1852
|
+
onDestroy(_dnd, session) {
|
|
1853
|
+
const map = session?.selectedByNs;
|
|
1854
|
+
if (!map) return;
|
|
1855
|
+
clearAll(map);
|
|
1856
|
+
}
|
|
1857
|
+
};
|
|
1858
|
+
//#endregion
|
|
1859
|
+
//#region src/services/MarqueeSelectionService.js
|
|
1860
|
+
function normalizeRect(x1, y1, x2, y2) {
|
|
1861
|
+
return {
|
|
1862
|
+
left: Math.min(x1, x2),
|
|
1863
|
+
right: Math.max(x1, x2),
|
|
1864
|
+
top: Math.min(y1, y2),
|
|
1865
|
+
bottom: Math.max(y1, y2),
|
|
1866
|
+
width: Math.abs(x2 - x1),
|
|
1867
|
+
height: Math.abs(y2 - y1)
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1870
|
+
function isHalfOverlap(selectRect, elementRect) {
|
|
1871
|
+
const overlapLeft = Math.max(selectRect.left, elementRect.left);
|
|
1872
|
+
const overlapRight = Math.min(selectRect.right, elementRect.right);
|
|
1873
|
+
const overlapTop = Math.max(selectRect.top, elementRect.top);
|
|
1874
|
+
const overlapBottom = Math.min(selectRect.bottom, elementRect.bottom);
|
|
1875
|
+
const overlapW = overlapRight - overlapLeft;
|
|
1876
|
+
const overlapH = overlapBottom - overlapTop;
|
|
1877
|
+
if (overlapW <= 0 || overlapH <= 0) return false;
|
|
1878
|
+
return overlapW * overlapH > (elementRect.right - elementRect.left) * (elementRect.bottom - elementRect.top) * MARQUEE_OVERLAP_RATIO;
|
|
1879
|
+
}
|
|
1880
|
+
var MarqueeSelectionService = class {
|
|
1881
|
+
order = -40;
|
|
1882
|
+
#dnd;
|
|
1883
|
+
#rootSignal;
|
|
1884
|
+
onAttach(dnd) {
|
|
1885
|
+
this.#dnd = dnd;
|
|
1886
|
+
}
|
|
1887
|
+
onRootChange(nextRoot, _prevRoot, signal) {
|
|
1888
|
+
this.#rootSignal = signal;
|
|
1889
|
+
if (!isHTMLElement(nextRoot)) return;
|
|
1890
|
+
nextRoot.addEventListener("pointerdown", (e) => {
|
|
1891
|
+
if (e.button !== 0) return;
|
|
1892
|
+
if (isIgnoreClick(e.target)) return;
|
|
1893
|
+
const scope = e.target.closest(SCOPE_SEL);
|
|
1894
|
+
if (!scope) return;
|
|
1895
|
+
if (this.#dnd.monitor.adapter.closestDraggable(e.target)) return;
|
|
1896
|
+
this.#startMarquee(scope, e);
|
|
1897
|
+
}, {
|
|
1898
|
+
capture: true,
|
|
1899
|
+
signal
|
|
1900
|
+
});
|
|
1901
|
+
}
|
|
1902
|
+
#startMarquee(scope, startEvent) {
|
|
1903
|
+
const startX = startEvent.clientX;
|
|
1904
|
+
const startY = startEvent.clientY;
|
|
1905
|
+
const adapter = this.#dnd.monitor.adapter;
|
|
1906
|
+
const namespace = adapter.getNamespace(scope);
|
|
1907
|
+
const scaleRatio = parseFloat(scope.getAttribute(ATTR.scaleRatioAttr) || "1");
|
|
1908
|
+
const marquee = document.createElement("div");
|
|
1909
|
+
marquee.classList.add(CLASS_NAMES.marquee);
|
|
1910
|
+
scope.appendChild(marquee);
|
|
1911
|
+
const scopeRect = scope.getBoundingClientRect();
|
|
1912
|
+
const candidates = [];
|
|
1913
|
+
for (const el of scope.querySelectorAll(`[${ATTR.dragAttr}], [${ATTR.dragdropAttr}]`)) if (adapter.getNamespace(el) === namespace) candidates.push(el);
|
|
1914
|
+
let hasMoved = false;
|
|
1915
|
+
let rafId = 0;
|
|
1916
|
+
let lastEvent = null;
|
|
1917
|
+
const rootSignal = this.#rootSignal;
|
|
1918
|
+
const sessionAbort = new AbortController();
|
|
1919
|
+
const onRootAbort = () => cleanup();
|
|
1920
|
+
const cleanup = () => {
|
|
1921
|
+
if (sessionAbort.signal.aborted) return;
|
|
1922
|
+
if (rafId) {
|
|
1923
|
+
cancelAnimationFrame(rafId);
|
|
1924
|
+
rafId = 0;
|
|
1925
|
+
}
|
|
1926
|
+
rootSignal?.removeEventListener?.("abort", onRootAbort);
|
|
1927
|
+
try {
|
|
1928
|
+
marquee.remove();
|
|
1929
|
+
} catch (_) {}
|
|
1930
|
+
sessionAbort.abort();
|
|
1931
|
+
};
|
|
1932
|
+
const processMove = () => {
|
|
1933
|
+
rafId = 0;
|
|
1934
|
+
const e = lastEvent;
|
|
1935
|
+
if (!e) return;
|
|
1936
|
+
const rect = normalizeRect(startX, startY, e.clientX, e.clientY);
|
|
1937
|
+
this.#updateMarqueeDOM(marquee, rect, scopeRect, scaleRatio);
|
|
1938
|
+
const selected = this.#detectCollisions(candidates, adapter, rect);
|
|
1939
|
+
const map = this.#dnd.monitor.selectedByNs;
|
|
1940
|
+
if (map) {
|
|
1941
|
+
clearAll(map, namespace);
|
|
1942
|
+
if (selected.length > 0) setMultipleActive(map, selected, namespace);
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
const onMove = (e) => {
|
|
1946
|
+
const dx = e.clientX - startX;
|
|
1947
|
+
const dy = e.clientY - startY;
|
|
1948
|
+
if (!hasMoved && Math.abs(dx) < THRESHOLDS.MARQUEE_MOVE && Math.abs(dy) < THRESHOLDS.MARQUEE_MOVE) return;
|
|
1949
|
+
hasMoved = true;
|
|
1950
|
+
lastEvent = e;
|
|
1951
|
+
if (!rafId) rafId = requestAnimationFrame(processMove);
|
|
1952
|
+
};
|
|
1953
|
+
const onUp = () => {
|
|
1954
|
+
if (!hasMoved) {
|
|
1955
|
+
const map = this.#dnd.monitor?.selectedByNs;
|
|
1956
|
+
if (map) clearAll(map, namespace);
|
|
1957
|
+
}
|
|
1958
|
+
cleanup();
|
|
1959
|
+
};
|
|
1960
|
+
if (rootSignal?.aborted) {
|
|
1961
|
+
cleanup();
|
|
1962
|
+
return;
|
|
1963
|
+
}
|
|
1964
|
+
rootSignal?.addEventListener?.("abort", onRootAbort, { once: true });
|
|
1965
|
+
const opts = { signal: sessionAbort.signal };
|
|
1966
|
+
document.addEventListener("pointermove", onMove, opts);
|
|
1967
|
+
document.addEventListener("pointerup", onUp, opts);
|
|
1968
|
+
document.addEventListener("pointercancel", cleanup, opts);
|
|
1969
|
+
window.addEventListener("blur", cleanup, opts);
|
|
1970
|
+
}
|
|
1971
|
+
#updateMarqueeDOM(marquee, rect, scopeRect, scaleRatio) {
|
|
1972
|
+
const left = (rect.left - scopeRect.left) / scaleRatio;
|
|
1973
|
+
const top = (rect.top - scopeRect.top) / scaleRatio;
|
|
1974
|
+
const width = rect.width / scaleRatio;
|
|
1975
|
+
const height = rect.height / scaleRatio;
|
|
1976
|
+
marquee.style.left = `${left}px`;
|
|
1977
|
+
marquee.style.top = `${top}px`;
|
|
1978
|
+
marquee.style.width = `${width}px`;
|
|
1979
|
+
marquee.style.height = `${height}px`;
|
|
1980
|
+
}
|
|
1981
|
+
#detectCollisions(candidates, adapter, selectRect) {
|
|
1982
|
+
const selected = [];
|
|
1983
|
+
for (const el of candidates) {
|
|
1984
|
+
const elRect = adapter.measureRect(el);
|
|
1985
|
+
if (elRect && isHalfOverlap(selectRect, elRect)) selected.push(el);
|
|
1986
|
+
}
|
|
1987
|
+
return selected;
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
//#endregion
|
|
1991
|
+
//#region src/services/transform/utils/geometry.js
|
|
1992
|
+
/**
|
|
1993
|
+
* 几何计算工具函数
|
|
1994
|
+
*/
|
|
1995
|
+
/**
|
|
1996
|
+
* 添加单位(px),保留亚像素精度(四舍五入到 2 位小数去除浮点噪声)
|
|
1997
|
+
*/
|
|
1998
|
+
function withUnit(v = 0) {
|
|
1999
|
+
const n = Number(v);
|
|
2000
|
+
if (!Number.isFinite(n)) return "0px";
|
|
2001
|
+
return `${Math.round(n * 100) / 100}px`;
|
|
2002
|
+
}
|
|
2003
|
+
/**
|
|
2004
|
+
* 角度转弧度
|
|
2005
|
+
*/
|
|
2006
|
+
function toRad(deg) {
|
|
2007
|
+
return deg * Math.PI / 180;
|
|
2008
|
+
}
|
|
2009
|
+
/**
|
|
2010
|
+
* 计算向量长度
|
|
2011
|
+
*/
|
|
2012
|
+
function getLength(x, y) {
|
|
2013
|
+
return Math.sqrt(x * x + y * y);
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* 网格吸附计算
|
|
2017
|
+
*/
|
|
2018
|
+
function calcGrid(diff, grid) {
|
|
2019
|
+
const g = Math.max(1, Number(grid) || 1);
|
|
2020
|
+
const r = Math.abs(diff) % g;
|
|
2021
|
+
const mul = diff > 0 ? g : -g;
|
|
2022
|
+
if (r > g / 2) return mul * Math.ceil(Math.abs(diff) / g);
|
|
2023
|
+
return mul * Math.floor(Math.abs(diff) / g);
|
|
2024
|
+
}
|
|
2025
|
+
/**
|
|
2026
|
+
* 根据缩放比例获取元素矩形
|
|
2027
|
+
*/
|
|
2028
|
+
function getRectByScale(el, scale = 1, adapter) {
|
|
2029
|
+
const r = adapter.measureRect(el);
|
|
2030
|
+
const s = Number(scale) || 1;
|
|
2031
|
+
return {
|
|
2032
|
+
bottom: r.bottom / s,
|
|
2033
|
+
height: r.height / s,
|
|
2034
|
+
left: r.left / s,
|
|
2035
|
+
right: r.right / s,
|
|
2036
|
+
top: r.top / s,
|
|
2037
|
+
width: r.width / s
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
/**
|
|
2041
|
+
* 计算指针相对中心点的角度
|
|
2042
|
+
*/
|
|
2043
|
+
function angleToPointer(cx, cy, px, py) {
|
|
2044
|
+
const dx = px - cx;
|
|
2045
|
+
const dy = py - cy;
|
|
2046
|
+
let deg = Math.atan2(dy, dx) * 180 / Math.PI + 90;
|
|
2047
|
+
deg = (deg % 360 + 360) % 360;
|
|
2048
|
+
return deg;
|
|
2049
|
+
}
|
|
2050
|
+
/**
|
|
2051
|
+
* 格式化变换数据(中心点坐标系)
|
|
2052
|
+
*/
|
|
2053
|
+
function formatData(data, cx, cy) {
|
|
2054
|
+
const { width, height } = data;
|
|
2055
|
+
return {
|
|
2056
|
+
height: Math.abs(height),
|
|
2057
|
+
width: Math.abs(width),
|
|
2058
|
+
x: cx - Math.abs(width) / 2,
|
|
2059
|
+
y: cy - Math.abs(height) / 2
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
/**
|
|
2063
|
+
* 中心点坐标转左上角坐标
|
|
2064
|
+
*/
|
|
2065
|
+
function centerToTL({ centerX, centerY, width, height, angle }) {
|
|
2066
|
+
return {
|
|
2067
|
+
angle,
|
|
2068
|
+
height,
|
|
2069
|
+
width,
|
|
2070
|
+
x: centerX - width / 2,
|
|
2071
|
+
y: centerY - height / 2
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
/**
|
|
2075
|
+
* 获取元素的 transform 信息
|
|
2076
|
+
*/
|
|
2077
|
+
function getElementTransformInfo(el) {
|
|
2078
|
+
const style = getComputedStyle(el);
|
|
2079
|
+
let matrix = {};
|
|
2080
|
+
try {
|
|
2081
|
+
matrix = new DOMMatrixReadOnly(style.transform);
|
|
2082
|
+
} catch {
|
|
2083
|
+
matrix = new DOMMatrixReadOnly();
|
|
2084
|
+
}
|
|
2085
|
+
const x = matrix.m41;
|
|
2086
|
+
const y = matrix.m42;
|
|
2087
|
+
let width = Number.parseFloat(style.width);
|
|
2088
|
+
let height = Number.parseFloat(style.height);
|
|
2089
|
+
if (!Number.isFinite(width) || !Number.isFinite(height)) {
|
|
2090
|
+
const r = el.getBoundingClientRect();
|
|
2091
|
+
width = r.width;
|
|
2092
|
+
height = r.height;
|
|
2093
|
+
}
|
|
2094
|
+
return {
|
|
2095
|
+
angle: -Math.atan2(matrix.m21, matrix.m11) * (180 / Math.PI),
|
|
2096
|
+
height,
|
|
2097
|
+
width,
|
|
2098
|
+
x,
|
|
2099
|
+
y
|
|
2100
|
+
};
|
|
2101
|
+
}
|
|
2102
|
+
//#endregion
|
|
2103
|
+
//#region src/services/transform/utils/cursor.js
|
|
2104
|
+
/**
|
|
2105
|
+
* 光标映射工具
|
|
2106
|
+
*/
|
|
2107
|
+
var resizableMap = {
|
|
2108
|
+
e: "right",
|
|
2109
|
+
n: "top",
|
|
2110
|
+
ne: "top-right",
|
|
2111
|
+
nw: "top-left",
|
|
2112
|
+
s: "bottom",
|
|
2113
|
+
se: "bottom-right",
|
|
2114
|
+
sw: "bottom-left",
|
|
2115
|
+
w: "left"
|
|
2116
|
+
};
|
|
2117
|
+
var cursorDir = [
|
|
2118
|
+
"n",
|
|
2119
|
+
"ne",
|
|
2120
|
+
"e",
|
|
2121
|
+
"se",
|
|
2122
|
+
"s",
|
|
2123
|
+
"sw",
|
|
2124
|
+
"w",
|
|
2125
|
+
"nw"
|
|
2126
|
+
];
|
|
2127
|
+
var cursorMap = {
|
|
2128
|
+
0: 0,
|
|
2129
|
+
1: 1,
|
|
2130
|
+
2: 1,
|
|
2131
|
+
3: 2,
|
|
2132
|
+
4: 3,
|
|
2133
|
+
5: 3,
|
|
2134
|
+
6: 4,
|
|
2135
|
+
7: 5,
|
|
2136
|
+
8: 5,
|
|
2137
|
+
9: 6,
|
|
2138
|
+
10: 7,
|
|
2139
|
+
11: 7
|
|
2140
|
+
};
|
|
2141
|
+
var cursorStartMap = {
|
|
2142
|
+
e: 2,
|
|
2143
|
+
n: 0,
|
|
2144
|
+
ne: 1,
|
|
2145
|
+
nw: 7,
|
|
2146
|
+
s: 4,
|
|
2147
|
+
se: 3,
|
|
2148
|
+
sw: 5,
|
|
2149
|
+
w: 6
|
|
2150
|
+
};
|
|
2151
|
+
/**
|
|
2152
|
+
* 根据旋转角度和方向获取光标样式
|
|
2153
|
+
*/
|
|
2154
|
+
function getCursor(angle, d) {
|
|
2155
|
+
const inc = cursorMap[Math.floor((angle % 360 + 360) % 360 / 30)];
|
|
2156
|
+
return cursorDir[(cursorStartMap[d] + inc) % 8];
|
|
2157
|
+
}
|
|
2158
|
+
//#endregion
|
|
2159
|
+
//#region src/services/transform/utils/markline.js
|
|
2160
|
+
/**
|
|
2161
|
+
* Markline(对齐辅助线)工具
|
|
2162
|
+
*/
|
|
2163
|
+
/**
|
|
2164
|
+
* 计算对齐线
|
|
2165
|
+
* @param {Array} list - 其他元素的位置信息列表
|
|
2166
|
+
* @param {Object} current - 当前元素的位置信息
|
|
2167
|
+
* @returns {Object} 对齐线信息
|
|
2168
|
+
*/
|
|
2169
|
+
function calcLines(list, current) {
|
|
2170
|
+
const lines = {
|
|
2171
|
+
x: [],
|
|
2172
|
+
y: []
|
|
2173
|
+
};
|
|
2174
|
+
const { width = 0, height = 0 } = current;
|
|
2175
|
+
list.forEach((b) => {
|
|
2176
|
+
const { top: ATop, left: ALeft, width: AWidth, height: AHeight } = b;
|
|
2177
|
+
lines.y.push({
|
|
2178
|
+
showTop: ATop,
|
|
2179
|
+
top: ATop
|
|
2180
|
+
});
|
|
2181
|
+
lines.y.push({
|
|
2182
|
+
showTop: ATop,
|
|
2183
|
+
top: ATop - height
|
|
2184
|
+
});
|
|
2185
|
+
lines.y.push({
|
|
2186
|
+
showTop: ATop + AHeight / 2,
|
|
2187
|
+
top: ATop + AHeight / 2 - height / 2
|
|
2188
|
+
});
|
|
2189
|
+
lines.y.push({
|
|
2190
|
+
showTop: ATop + AHeight,
|
|
2191
|
+
top: ATop + AHeight
|
|
2192
|
+
});
|
|
2193
|
+
lines.y.push({
|
|
2194
|
+
showTop: ATop + AHeight,
|
|
2195
|
+
top: ATop + AHeight - height
|
|
2196
|
+
});
|
|
2197
|
+
lines.x.push({
|
|
2198
|
+
left: ALeft,
|
|
2199
|
+
showLeft: ALeft
|
|
2200
|
+
});
|
|
2201
|
+
lines.x.push({
|
|
2202
|
+
left: ALeft + AWidth,
|
|
2203
|
+
showLeft: ALeft + AWidth
|
|
2204
|
+
});
|
|
2205
|
+
lines.x.push({
|
|
2206
|
+
left: ALeft + AWidth / 2 - width / 2,
|
|
2207
|
+
showLeft: ALeft + AWidth / 2
|
|
2208
|
+
});
|
|
2209
|
+
lines.x.push({
|
|
2210
|
+
left: ALeft + AWidth - width,
|
|
2211
|
+
showLeft: ALeft + AWidth
|
|
2212
|
+
});
|
|
2213
|
+
lines.x.push({
|
|
2214
|
+
left: ALeft - width,
|
|
2215
|
+
showLeft: ALeft
|
|
2216
|
+
});
|
|
2217
|
+
});
|
|
2218
|
+
return lines;
|
|
2219
|
+
}
|
|
2220
|
+
/**
|
|
2221
|
+
* Resize 专用吸附解算(纯函数)
|
|
2222
|
+
*
|
|
2223
|
+
* 与 calcLines(为 move 设计、把当前 width/height 烘进候选线)不同:
|
|
2224
|
+
* resize 只把「正在拖动的那条边」吸附到邻居的裸边(left/中线/right,top/中线/bottom),
|
|
2225
|
+
* 对侧边保持锚定。返回移动边需要的位移 diff 与命中线在 viewport 坐标的位置 guide。
|
|
2226
|
+
*
|
|
2227
|
+
* @param {Object} arg
|
|
2228
|
+
* @param {string} arg.side - left/right/top/bottom/top-left/top-right/bottom-left/bottom-right
|
|
2229
|
+
* @param {Object} arg.source - 当前盒子矩形(viewport-scaled,含 left/right/top/bottom)
|
|
2230
|
+
* @param {Array} arg.targetRects - 邻居元素矩形列表(viewport-scaled,含 left/top/width/height)
|
|
2231
|
+
* @param {number} arg.threshold - 吸附阈值(viewport-scaled 像素)
|
|
2232
|
+
* @returns {{diffX:number, diffY:number, guideLeft:(number|undefined), guideTop:(number|undefined)}}
|
|
2233
|
+
*/
|
|
2234
|
+
function resolveResizeSnap({ side = "", source, targetRects = [], threshold = 10 }) {
|
|
2235
|
+
const result = {
|
|
2236
|
+
diffX: 0,
|
|
2237
|
+
diffY: 0,
|
|
2238
|
+
guideLeft: void 0,
|
|
2239
|
+
guideTop: void 0
|
|
2240
|
+
};
|
|
2241
|
+
if (!source) return result;
|
|
2242
|
+
const movesLeft = side.includes("left");
|
|
2243
|
+
const movesRight = side.includes("right");
|
|
2244
|
+
const movesTop = side.includes("top");
|
|
2245
|
+
const movesBottom = side.includes("bottom");
|
|
2246
|
+
if (movesLeft || movesRight) {
|
|
2247
|
+
const edge = movesLeft ? source.left : source.right;
|
|
2248
|
+
for (const t of targetRects) {
|
|
2249
|
+
const candidates = [
|
|
2250
|
+
t.left,
|
|
2251
|
+
t.left + t.width / 2,
|
|
2252
|
+
t.left + t.width
|
|
2253
|
+
];
|
|
2254
|
+
let hit;
|
|
2255
|
+
for (const c of candidates) if (Math.abs(c - edge) < threshold) {
|
|
2256
|
+
hit = c;
|
|
2257
|
+
break;
|
|
2258
|
+
}
|
|
2259
|
+
if (hit !== void 0) {
|
|
2260
|
+
result.diffX = hit - edge;
|
|
2261
|
+
result.guideLeft = hit;
|
|
2262
|
+
break;
|
|
2263
|
+
}
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
if (movesTop || movesBottom) {
|
|
2267
|
+
const edge = movesTop ? source.top : source.bottom;
|
|
2268
|
+
for (const t of targetRects) {
|
|
2269
|
+
const candidates = [
|
|
2270
|
+
t.top,
|
|
2271
|
+
t.top + t.height / 2,
|
|
2272
|
+
t.top + t.height
|
|
2273
|
+
];
|
|
2274
|
+
let hit;
|
|
2275
|
+
for (const c of candidates) if (Math.abs(c - edge) < threshold) {
|
|
2276
|
+
hit = c;
|
|
2277
|
+
break;
|
|
2278
|
+
}
|
|
2279
|
+
if (hit !== void 0) {
|
|
2280
|
+
result.diffY = hit - edge;
|
|
2281
|
+
result.guideTop = hit;
|
|
2282
|
+
break;
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
return result;
|
|
2287
|
+
}
|
|
2288
|
+
/**
|
|
2289
|
+
* 创建临时对齐线容器
|
|
2290
|
+
*/
|
|
2291
|
+
function createTempMarklines(container) {
|
|
2292
|
+
let x = container.querySelector(".draggable-markline-x");
|
|
2293
|
+
let y = container.querySelector(".draggable-markline-y");
|
|
2294
|
+
if (!x) {
|
|
2295
|
+
x = document.createElement("div");
|
|
2296
|
+
x.className = "draggable-markline-x";
|
|
2297
|
+
container.appendChild(x);
|
|
2298
|
+
}
|
|
2299
|
+
if (!y) {
|
|
2300
|
+
y = document.createElement("div");
|
|
2301
|
+
y.className = "draggable-markline-y";
|
|
2302
|
+
container.appendChild(y);
|
|
2303
|
+
}
|
|
2304
|
+
return {
|
|
2305
|
+
x,
|
|
2306
|
+
y
|
|
2307
|
+
};
|
|
2308
|
+
}
|
|
2309
|
+
/**
|
|
2310
|
+
* 隐藏对齐线
|
|
2311
|
+
*/
|
|
2312
|
+
function hideMarkline(el) {
|
|
2313
|
+
if (!el) return;
|
|
2314
|
+
el.style.display = "none";
|
|
2315
|
+
}
|
|
2316
|
+
//#endregion
|
|
2317
|
+
//#region src/services/transform/utils/resize.js
|
|
2318
|
+
/**
|
|
2319
|
+
* Resize 尺寸计算工具
|
|
2320
|
+
*/
|
|
2321
|
+
/**
|
|
2322
|
+
* 设置宽度和增量,考虑最小宽度限制
|
|
2323
|
+
*/
|
|
2324
|
+
function setWidthAndDeltaW(width, deltaW, minWidth) {
|
|
2325
|
+
const expected = width + deltaW;
|
|
2326
|
+
if (expected > minWidth) width = expected;
|
|
2327
|
+
else {
|
|
2328
|
+
deltaW = minWidth - width;
|
|
2329
|
+
width = minWidth;
|
|
2330
|
+
}
|
|
2331
|
+
return {
|
|
2332
|
+
deltaW,
|
|
2333
|
+
width
|
|
2334
|
+
};
|
|
2335
|
+
}
|
|
2336
|
+
/**
|
|
2337
|
+
* 设置高度和增量,考虑最小高度限制
|
|
2338
|
+
*/
|
|
2339
|
+
function setHeightAndDeltaH(height, deltaH, minHeight) {
|
|
2340
|
+
const expected = height + deltaH;
|
|
2341
|
+
if (expected > minHeight) height = expected;
|
|
2342
|
+
else {
|
|
2343
|
+
deltaH = minHeight - height;
|
|
2344
|
+
height = minHeight;
|
|
2345
|
+
}
|
|
2346
|
+
return {
|
|
2347
|
+
deltaH,
|
|
2348
|
+
height
|
|
2349
|
+
};
|
|
2350
|
+
}
|
|
2351
|
+
/**
|
|
2352
|
+
* 根据 resize 类型计算新的样式
|
|
2353
|
+
* @param {string} type - resize 方向(top/right/bottom/left/top-right/...)
|
|
2354
|
+
* @param {Object} rect - 当前矩形信息
|
|
2355
|
+
* @param {number} deltaW - 宽度变化量
|
|
2356
|
+
* @param {number} deltaH - 高度变化量
|
|
2357
|
+
* @param {number} ratio - 宽高比(可选)
|
|
2358
|
+
* @param {number} minWidth - 最小宽度
|
|
2359
|
+
* @param {number} minHeight - 最小高度
|
|
2360
|
+
* @returns {Object} 新的样式信息
|
|
2361
|
+
*/
|
|
2362
|
+
function getNewStyle(type, rect, deltaW, deltaH, ratio, minWidth, minHeight) {
|
|
2363
|
+
let { width, height, centerX, centerY, rotateAngle } = rect;
|
|
2364
|
+
const wf = width < 0 ? -1 : 1;
|
|
2365
|
+
const hf = height < 0 ? -1 : 1;
|
|
2366
|
+
width = Math.abs(width);
|
|
2367
|
+
height = Math.abs(height);
|
|
2368
|
+
const rad = toRad(rotateAngle);
|
|
2369
|
+
const cos = Math.cos(rad);
|
|
2370
|
+
const sin = Math.sin(rad);
|
|
2371
|
+
if ([
|
|
2372
|
+
"top-left",
|
|
2373
|
+
"top-right",
|
|
2374
|
+
"bottom-left",
|
|
2375
|
+
"bottom-right"
|
|
2376
|
+
].includes(type)) {
|
|
2377
|
+
if (type === "top-right") deltaH = -deltaH;
|
|
2378
|
+
else if (type === "bottom-left") deltaW = -deltaW;
|
|
2379
|
+
else if (type === "top-left") {
|
|
2380
|
+
deltaW = -deltaW;
|
|
2381
|
+
deltaH = -deltaH;
|
|
2382
|
+
}
|
|
2383
|
+
({width, deltaW} = setWidthAndDeltaW(width, deltaW, minWidth));
|
|
2384
|
+
({height, deltaH} = setHeightAndDeltaH(height, deltaH, minHeight));
|
|
2385
|
+
if (ratio) {
|
|
2386
|
+
deltaH = deltaW / ratio;
|
|
2387
|
+
height = width / ratio;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
switch (type) {
|
|
2391
|
+
case "right":
|
|
2392
|
+
({width, deltaW} = setWidthAndDeltaW(width, deltaW, minWidth));
|
|
2393
|
+
if (ratio) {
|
|
2394
|
+
deltaH = deltaW / ratio;
|
|
2395
|
+
height = width / ratio;
|
|
2396
|
+
centerX += deltaW / 2 * cos - deltaH / 2 * sin;
|
|
2397
|
+
centerY += deltaW / 2 * sin + deltaH / 2 * cos;
|
|
2398
|
+
} else {
|
|
2399
|
+
centerX += deltaW / 2 * cos;
|
|
2400
|
+
centerY += deltaW / 2 * sin;
|
|
2401
|
+
}
|
|
2402
|
+
break;
|
|
2403
|
+
case "top-right":
|
|
2404
|
+
centerX += deltaW / 2 * cos + deltaH / 2 * sin;
|
|
2405
|
+
centerY += deltaW / 2 * sin - deltaH / 2 * cos;
|
|
2406
|
+
break;
|
|
2407
|
+
case "bottom-right":
|
|
2408
|
+
centerX += deltaW / 2 * cos - deltaH / 2 * sin;
|
|
2409
|
+
centerY += deltaW / 2 * sin + deltaH / 2 * cos;
|
|
2410
|
+
break;
|
|
2411
|
+
case "bottom":
|
|
2412
|
+
({height, deltaH} = setHeightAndDeltaH(height, deltaH, minHeight));
|
|
2413
|
+
if (ratio) {
|
|
2414
|
+
deltaW = deltaH * ratio;
|
|
2415
|
+
width = height * ratio;
|
|
2416
|
+
centerX += deltaW / 2 * cos - deltaH / 2 * sin;
|
|
2417
|
+
centerY += deltaW / 2 * sin + deltaH / 2 * cos;
|
|
2418
|
+
} else {
|
|
2419
|
+
centerX -= deltaH / 2 * sin;
|
|
2420
|
+
centerY += deltaH / 2 * cos;
|
|
2421
|
+
}
|
|
2422
|
+
break;
|
|
2423
|
+
case "bottom-left":
|
|
2424
|
+
centerX -= deltaW / 2 * cos + deltaH / 2 * sin;
|
|
2425
|
+
centerY -= deltaW / 2 * sin - deltaH / 2 * cos;
|
|
2426
|
+
break;
|
|
2427
|
+
case "left":
|
|
2428
|
+
deltaW = -deltaW;
|
|
2429
|
+
({width, deltaW} = setWidthAndDeltaW(width, deltaW, minWidth));
|
|
2430
|
+
if (ratio) {
|
|
2431
|
+
height = width / ratio;
|
|
2432
|
+
deltaH = deltaW / ratio;
|
|
2433
|
+
centerX -= deltaW / 2 * cos + deltaH / 2 * sin;
|
|
2434
|
+
centerY -= deltaW / 2 * sin - deltaH / 2 * cos;
|
|
2435
|
+
} else {
|
|
2436
|
+
centerX -= deltaW / 2 * cos;
|
|
2437
|
+
centerY -= deltaW / 2 * sin;
|
|
2438
|
+
}
|
|
2439
|
+
break;
|
|
2440
|
+
case "top-left":
|
|
2441
|
+
centerX -= deltaW / 2 * cos - deltaH / 2 * sin;
|
|
2442
|
+
centerY -= deltaW / 2 * sin + deltaH / 2 * cos;
|
|
2443
|
+
break;
|
|
2444
|
+
case "top":
|
|
2445
|
+
deltaH = -deltaH;
|
|
2446
|
+
({height, deltaH} = setHeightAndDeltaH(height, deltaH, minHeight));
|
|
2447
|
+
if (ratio) {
|
|
2448
|
+
width = height * ratio;
|
|
2449
|
+
deltaW = deltaH * ratio;
|
|
2450
|
+
centerX += deltaW / 2 * cos + deltaH / 2 * sin;
|
|
2451
|
+
centerY += deltaW / 2 * sin - deltaH / 2 * cos;
|
|
2452
|
+
} else {
|
|
2453
|
+
centerX += deltaH / 2 * sin;
|
|
2454
|
+
centerY -= deltaH / 2 * cos;
|
|
2455
|
+
}
|
|
2456
|
+
break;
|
|
2457
|
+
default: break;
|
|
2458
|
+
}
|
|
2459
|
+
return {
|
|
2460
|
+
position: {
|
|
2461
|
+
centerX,
|
|
2462
|
+
centerY
|
|
2463
|
+
},
|
|
2464
|
+
size: {
|
|
2465
|
+
height: height * hf,
|
|
2466
|
+
width: width * wf
|
|
2467
|
+
}
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2471
|
+
//#region src/services/transform/utils/handles.js
|
|
2472
|
+
/**
|
|
2473
|
+
* Handle(控制点)管理工具
|
|
2474
|
+
*/
|
|
2475
|
+
/**
|
|
2476
|
+
* 查找元素内的所有 resize 控制点
|
|
2477
|
+
*/
|
|
2478
|
+
function findDots(el) {
|
|
2479
|
+
const wrap = el?.querySelector?.(`:scope > .draggable-dot-wrap`);
|
|
2480
|
+
return wrap ? [...wrap.querySelectorAll?.(`.draggable-dot`) || []] : [];
|
|
2481
|
+
}
|
|
2482
|
+
/**
|
|
2483
|
+
* 从 handle 元素获取 resize 方向
|
|
2484
|
+
*/
|
|
2485
|
+
function getResizeSideFromHandle(handleEl) {
|
|
2486
|
+
const dir = handleEl?.dataset?.dir;
|
|
2487
|
+
return dir && resizableMap[dir] || "right";
|
|
2488
|
+
}
|
|
2489
|
+
/**
|
|
2490
|
+
* 判断是否为 rotate handle
|
|
2491
|
+
*/
|
|
2492
|
+
function isRotateHandle(handleEl) {
|
|
2493
|
+
return Boolean(handleEl?.hasAttribute?.(ATTR.handleRotateAttr));
|
|
2494
|
+
}
|
|
2495
|
+
/**
|
|
2496
|
+
* 判断是否为 resize handle
|
|
2497
|
+
*/
|
|
2498
|
+
function isResizeHandle(handleEl) {
|
|
2499
|
+
return Boolean(handleEl?.hasAttribute?.(ATTR.handleResizeAttr));
|
|
2500
|
+
}
|
|
2501
|
+
//#endregion
|
|
2502
|
+
//#region src/services/TransformControllerService.js
|
|
2503
|
+
var TransformControllerService = class {
|
|
2504
|
+
order = 25;
|
|
2505
|
+
#opt;
|
|
2506
|
+
constructor(options = {}) {
|
|
2507
|
+
this.#opt = {
|
|
2508
|
+
resizable: true,
|
|
2509
|
+
rotatable: true,
|
|
2510
|
+
/** 是否限制在 drag-scope 容器内 */
|
|
2511
|
+
boundary: true,
|
|
2512
|
+
/** 外层缩放比例(画布缩放) */
|
|
2513
|
+
scaleRatio: 1,
|
|
2514
|
+
/** 吸附到网格 */
|
|
2515
|
+
snapToGrid: false,
|
|
2516
|
+
gridX: 10,
|
|
2517
|
+
gridY: 10,
|
|
2518
|
+
/** resize 约束 */
|
|
2519
|
+
aspectRatio: void 0,
|
|
2520
|
+
minWidth: 10,
|
|
2521
|
+
minHeight: 10,
|
|
2522
|
+
maxWidth: 0,
|
|
2523
|
+
maxHeight: 0,
|
|
2524
|
+
/** rotate 吸附 */
|
|
2525
|
+
rotateSnap: false,
|
|
2526
|
+
rotateStep: 15,
|
|
2527
|
+
/** markline + snap */
|
|
2528
|
+
snap: true,
|
|
2529
|
+
snapThreshold: 10,
|
|
2530
|
+
markline: true,
|
|
2531
|
+
/** 在 drag-scope 内默认关闭镜像与 drop 指示器 */
|
|
2532
|
+
disableMirrorInScope: true,
|
|
2533
|
+
disableDropIndicatorInScope: true,
|
|
2534
|
+
/** 是否允许 transform(业务可覆写) */
|
|
2535
|
+
canTransform: () => true,
|
|
2536
|
+
...options
|
|
2537
|
+
};
|
|
2538
|
+
}
|
|
2539
|
+
/**
|
|
2540
|
+
* 获取当前 drag-scope/drop 容器的缩放比例:
|
|
2541
|
+
* - 优先读取容器上的 [scale-ratio](或 [data-scale-ratio])
|
|
2542
|
+
* - 若未设置/非法,则回退到 options.scaleRatio
|
|
2543
|
+
*/
|
|
2544
|
+
#resolveScaleRatio(container, adapter) {
|
|
2545
|
+
const name = ATTR.scaleRatioAttr || "scale-ratio";
|
|
2546
|
+
const v = adapter?.getAttr?.(container, name) ?? adapter?.getAttr?.(container, `data-${name}`) ?? container?.getAttribute?.(name) ?? container?.getAttribute?.(`data-${name}`);
|
|
2547
|
+
const n = Number.parseFloat(String(v ?? ""));
|
|
2548
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
2549
|
+
const opt = Number(this.#opt.scaleRatio);
|
|
2550
|
+
return Number.isFinite(opt) && opt > 0 ? opt : 1;
|
|
2551
|
+
}
|
|
2552
|
+
#getScaleRatio(st) {
|
|
2553
|
+
const n = Number(st?.scaleRatio);
|
|
2554
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
2555
|
+
const opt = Number(this.#opt.scaleRatio);
|
|
2556
|
+
return Number.isFinite(opt) && opt > 0 ? opt : 1;
|
|
2557
|
+
}
|
|
2558
|
+
onDown(ctx) {
|
|
2559
|
+
const m = ctx?.session;
|
|
2560
|
+
if (!m?.isDragScope || !isHTMLElement(m.sourceEl)) return;
|
|
2561
|
+
if (!this.#opt.resizable) return;
|
|
2562
|
+
const { angle } = getElementTransformInfo(m.sourceEl);
|
|
2563
|
+
if (angle) this.#updateDotCursors(m.sourceEl, angle);
|
|
2564
|
+
}
|
|
2565
|
+
onStart(ctx) {
|
|
2566
|
+
const m = ctx.session;
|
|
2567
|
+
if (!m?.active || !m?.started || !m.isDragScope) return;
|
|
2568
|
+
if (!isHTMLElement(m.sourceEl)) return;
|
|
2569
|
+
if (typeof this.#opt.canTransform === "function" && !this.#opt.canTransform(m.sourceEl)) return;
|
|
2570
|
+
const selectedValue = m.selectedByNs?.get(m.namespace);
|
|
2571
|
+
const isMultiSelect = Array.isArray(selectedValue) && selectedValue.length > 1;
|
|
2572
|
+
let type = "move";
|
|
2573
|
+
let side = "";
|
|
2574
|
+
const canRotate = this.#opt.rotatable && isToggleEnabled(m.sourceEl, ATTR.rotatableAttr, true);
|
|
2575
|
+
const canResize = this.#opt.resizable && isToggleEnabled(m.sourceEl, ATTR.resizableAttr, true);
|
|
2576
|
+
if (isRotateHandle(m.handleEl) && canRotate) {
|
|
2577
|
+
if (isMultiSelect) return;
|
|
2578
|
+
type = "rotate";
|
|
2579
|
+
} else if (isResizeHandle(m.handleEl) && canResize) {
|
|
2580
|
+
if (isMultiSelect) return;
|
|
2581
|
+
type = "resize";
|
|
2582
|
+
side = getResizeSideFromHandle(m.handleEl);
|
|
2583
|
+
}
|
|
2584
|
+
if (this.#opt.disableMirrorInScope) this.#disableMirrorNow(m);
|
|
2585
|
+
if (this.#opt.disableDropIndicatorInScope) this.#disableIndicatorNow(m);
|
|
2586
|
+
m.cache.__transform = {
|
|
2587
|
+
phase: 0,
|
|
2588
|
+
type,
|
|
2589
|
+
side,
|
|
2590
|
+
scaleRatio: void 0,
|
|
2591
|
+
container: void 0,
|
|
2592
|
+
containerRect: void 0,
|
|
2593
|
+
containerRectScaled: void 0,
|
|
2594
|
+
boundary: void 0,
|
|
2595
|
+
start: void 0,
|
|
2596
|
+
data: void 0,
|
|
2597
|
+
center: void 0,
|
|
2598
|
+
rotateOffset: 0,
|
|
2599
|
+
startRect: void 0,
|
|
2600
|
+
viewportCenterOffset: void 0,
|
|
2601
|
+
lines: void 0,
|
|
2602
|
+
guideX: void 0,
|
|
2603
|
+
guideY: void 0,
|
|
2604
|
+
mark: {
|
|
2605
|
+
diffX: 0,
|
|
2606
|
+
diffY: 0,
|
|
2607
|
+
left: void 0,
|
|
2608
|
+
top: void 0
|
|
2609
|
+
},
|
|
2610
|
+
didCompute: false,
|
|
2611
|
+
lastValidData: void 0,
|
|
2612
|
+
lastAppliedX: void 0,
|
|
2613
|
+
lastAppliedY: void 0,
|
|
2614
|
+
lastAppliedW: void 0,
|
|
2615
|
+
lastAppliedH: void 0,
|
|
2616
|
+
lastAppliedAngle: void 0,
|
|
2617
|
+
multiSelection: {
|
|
2618
|
+
enabled: isMultiSelect,
|
|
2619
|
+
elements: [],
|
|
2620
|
+
initialTransforms: []
|
|
2621
|
+
}
|
|
2622
|
+
};
|
|
2623
|
+
}
|
|
2624
|
+
onMeasure(ctx) {
|
|
2625
|
+
const m = ctx.session;
|
|
2626
|
+
const st = m?.cache?.__transform;
|
|
2627
|
+
if (!m?.active || !m?.started || !m.isDragScope || !st || st.phase !== 0) return;
|
|
2628
|
+
if (!isHTMLElement(m.sourceEl)) {
|
|
2629
|
+
m.cache.__transform = void 0;
|
|
2630
|
+
return;
|
|
2631
|
+
}
|
|
2632
|
+
const container = ctx.adapter.closestDragScope(m.sourceEl);
|
|
2633
|
+
if (!isHTMLElement(container)) {
|
|
2634
|
+
m.cache.__transform = void 0;
|
|
2635
|
+
return;
|
|
2636
|
+
}
|
|
2637
|
+
const scale = this.#resolveScaleRatio(container, ctx.adapter);
|
|
2638
|
+
const info = getElementTransformInfo(m.sourceEl);
|
|
2639
|
+
const cRect = ctx.adapter.measureRect(container);
|
|
2640
|
+
const cRectScaled = {
|
|
2641
|
+
height: cRect.height / scale,
|
|
2642
|
+
left: cRect.left / scale,
|
|
2643
|
+
top: cRect.top / scale,
|
|
2644
|
+
width: cRect.width / scale
|
|
2645
|
+
};
|
|
2646
|
+
st.container = container;
|
|
2647
|
+
st.scaleRatio = scale;
|
|
2648
|
+
st.containerRect = cRect;
|
|
2649
|
+
st.containerRectScaled = cRectScaled;
|
|
2650
|
+
st.start = { ...info };
|
|
2651
|
+
st.data = { ...info };
|
|
2652
|
+
st.center = {
|
|
2653
|
+
x: info.x + info.width / 2,
|
|
2654
|
+
y: info.y + info.height / 2
|
|
2655
|
+
};
|
|
2656
|
+
if (this.#opt.boundary) st.boundary = {
|
|
2657
|
+
w: Math.max(0, container.clientWidth),
|
|
2658
|
+
h: Math.max(0, container.clientHeight)
|
|
2659
|
+
};
|
|
2660
|
+
if (st.type === "rotate") {
|
|
2661
|
+
const mx = (m.x - cRect.left) / scale;
|
|
2662
|
+
const my = (m.y - cRect.top) / scale;
|
|
2663
|
+
st.rotateOffset = angleToPointer(st.center.x, st.center.y, mx, my) - st.data.angle;
|
|
2664
|
+
}
|
|
2665
|
+
if (this.#opt.snap || this.#opt.markline) {
|
|
2666
|
+
const startRect = getRectByScale(m.sourceEl, scale, ctx.adapter);
|
|
2667
|
+
st.startRect = startRect;
|
|
2668
|
+
st.viewportCenterOffset = {
|
|
2669
|
+
x: startRect.left + startRect.width / 2 - st.center.x,
|
|
2670
|
+
y: startRect.top + startRect.height / 2 - st.center.y
|
|
2671
|
+
};
|
|
2672
|
+
const els = [...container.querySelectorAll?.(`[${ATTR.dragAttr}]`) || []];
|
|
2673
|
+
const targets = [];
|
|
2674
|
+
for (const node of els) {
|
|
2675
|
+
if (node === m.sourceEl) continue;
|
|
2676
|
+
if (!isHTMLElement(node)) continue;
|
|
2677
|
+
targets.push(getRectByScale(node, scale, ctx.adapter));
|
|
2678
|
+
}
|
|
2679
|
+
st.lines = calcLines(targets, startRect);
|
|
2680
|
+
st.targetRects = targets;
|
|
2681
|
+
if (this.#opt.markline) {
|
|
2682
|
+
const guides = createTempMarklines(container);
|
|
2683
|
+
st.guideX = guides.x;
|
|
2684
|
+
st.guideY = guides.y;
|
|
2685
|
+
}
|
|
2686
|
+
}
|
|
2687
|
+
if (st.multiSelection.enabled) {
|
|
2688
|
+
const selectedValue = m.selectedByNs?.get(m.namespace);
|
|
2689
|
+
if (Array.isArray(selectedValue)) {
|
|
2690
|
+
for (const el of selectedValue) if (el !== m.sourceEl && isHTMLElement(el)) {
|
|
2691
|
+
st.multiSelection.elements.push(el);
|
|
2692
|
+
st.multiSelection.initialTransforms.push(getElementTransformInfo(el));
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
}
|
|
2696
|
+
st.phase = 1;
|
|
2697
|
+
}
|
|
2698
|
+
onCompute(ctx) {
|
|
2699
|
+
const m = ctx.session;
|
|
2700
|
+
const st = m?.cache?.__transform;
|
|
2701
|
+
if (!m?.active || !m?.started || !m.isDragScope || !st || st.phase !== 1) return;
|
|
2702
|
+
if (!isHTMLElement(m.sourceEl)) return;
|
|
2703
|
+
st.didCompute = true;
|
|
2704
|
+
if (st.type === "move") this.#computeMove(m, st);
|
|
2705
|
+
else if (st.type === "rotate") this.#computeRotate(m, st);
|
|
2706
|
+
else if (st.type === "resize") this.#computeResize(m, st);
|
|
2707
|
+
}
|
|
2708
|
+
onCommit(ctx) {
|
|
2709
|
+
const m = ctx.session;
|
|
2710
|
+
const st = m?.cache?.__transform;
|
|
2711
|
+
if (!m?.active || !m?.started || !m.isDragScope || !st || st.phase !== 1) return;
|
|
2712
|
+
if (!st.didCompute || !isHTMLElement(m.sourceEl)) return;
|
|
2713
|
+
this.#apply(ctx, m.sourceEl, st);
|
|
2714
|
+
if (st.multiSelection.enabled && st.type === "move") {
|
|
2715
|
+
const deltaX = st.data.x - st.start.x;
|
|
2716
|
+
const deltaY = st.data.y - st.start.y;
|
|
2717
|
+
for (let i = 0; i < st.multiSelection.elements.length; i++) {
|
|
2718
|
+
const el = st.multiSelection.elements[i];
|
|
2719
|
+
const initialInfo = st.multiSelection.initialTransforms[i];
|
|
2720
|
+
const newData = {
|
|
2721
|
+
x: initialInfo.x + deltaX,
|
|
2722
|
+
y: initialInfo.y + deltaY,
|
|
2723
|
+
width: initialInfo.width,
|
|
2724
|
+
height: initialInfo.height,
|
|
2725
|
+
angle: initialInfo.angle
|
|
2726
|
+
};
|
|
2727
|
+
this.#applyTransform(ctx, el, newData);
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
if (st.type === "rotate" && this.#opt.resizable && st.lastCursorAngle !== st.data?.angle) {
|
|
2731
|
+
st.lastCursorAngle = st.data?.angle;
|
|
2732
|
+
ctx.frame?.run?.(() => {
|
|
2733
|
+
this.#updateDotCursors(m.sourceEl, st.lastCursorAngle || 0);
|
|
2734
|
+
});
|
|
2735
|
+
}
|
|
2736
|
+
if (this.#opt.markline) this.#commitMarkline(ctx, st);
|
|
2737
|
+
const dnd = ctx.dnd;
|
|
2738
|
+
if (dnd?.emit && st?.data) {
|
|
2739
|
+
let payload = st.__emitPayload;
|
|
2740
|
+
if (!payload) {
|
|
2741
|
+
payload = st.__emitPayload = {
|
|
2742
|
+
type: st.type,
|
|
2743
|
+
el: m.sourceEl
|
|
2744
|
+
};
|
|
2745
|
+
st.__emitEvt = st.type === "move" ? "draggable:drag" : st.type === "rotate" ? "draggable:rotate" : "draggable:resize";
|
|
2746
|
+
st.__emitRun = () => dnd.emit(st.__emitEvt, st.__emitPayload);
|
|
2747
|
+
}
|
|
2748
|
+
payload.el = m.sourceEl;
|
|
2749
|
+
payload.x = st.data.x;
|
|
2750
|
+
payload.y = st.data.y;
|
|
2751
|
+
payload.width = st.data.width;
|
|
2752
|
+
payload.height = st.data.height;
|
|
2753
|
+
payload.angle = st.data.angle;
|
|
2754
|
+
ctx.frame?.run?.(st.__emitRun);
|
|
2755
|
+
}
|
|
2756
|
+
st.didCompute = false;
|
|
2757
|
+
}
|
|
2758
|
+
onEnd(ctx, meta) {
|
|
2759
|
+
const m = ctx.session;
|
|
2760
|
+
const st = m?.cache?.__transform;
|
|
2761
|
+
if (!st) return;
|
|
2762
|
+
hideMarkline(st.guideX);
|
|
2763
|
+
hideMarkline(st.guideY);
|
|
2764
|
+
if (meta?.ended && isHTMLElement(m?.sourceEl) && st?.data) {
|
|
2765
|
+
ctx.dnd?.emit?.("draggable:drop", {
|
|
2766
|
+
type: st.type,
|
|
2767
|
+
el: m.sourceEl,
|
|
2768
|
+
data: m.data,
|
|
2769
|
+
...st.data
|
|
2770
|
+
});
|
|
2771
|
+
if (st.multiSelection?.enabled && st.type === "move") {
|
|
2772
|
+
const deltaX = st.data.x - st.start.x;
|
|
2773
|
+
const deltaY = st.data.y - st.start.y;
|
|
2774
|
+
const elements = [{
|
|
2775
|
+
el: m.sourceEl,
|
|
2776
|
+
x: st.data.x,
|
|
2777
|
+
y: st.data.y,
|
|
2778
|
+
width: st.data.width,
|
|
2779
|
+
height: st.data.height,
|
|
2780
|
+
angle: st.data.angle,
|
|
2781
|
+
data: m.data
|
|
2782
|
+
}, ...st.multiSelection.elements.map((el, i) => {
|
|
2783
|
+
const init = st.multiSelection.initialTransforms[i];
|
|
2784
|
+
return {
|
|
2785
|
+
el,
|
|
2786
|
+
x: init.x + deltaX,
|
|
2787
|
+
y: init.y + deltaY,
|
|
2788
|
+
width: init.width,
|
|
2789
|
+
height: init.height,
|
|
2790
|
+
angle: init.angle,
|
|
2791
|
+
data: parseData(ctx.adapter?.getAttr?.(el, ATTR.dataAttr))
|
|
2792
|
+
};
|
|
2793
|
+
})];
|
|
2794
|
+
ctx.dnd?.emit?.("draggable:multi-drop", elements);
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
if (meta?.ended && st.type === "rotate" && this.#opt.resizable && isHTMLElement(m?.sourceEl)) this.#updateDotCursors(m.sourceEl, st.data?.angle || 0);
|
|
2798
|
+
m.cache.__transform = void 0;
|
|
2799
|
+
}
|
|
2800
|
+
onDestroy(_dnd, session) {
|
|
2801
|
+
const st = session?.cache?.__transform;
|
|
2802
|
+
if (st) {
|
|
2803
|
+
hideMarkline(st.guideX);
|
|
2804
|
+
hideMarkline(st.guideY);
|
|
2805
|
+
session.cache.__transform = void 0;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
#computeMove(m, st) {
|
|
2809
|
+
const scale = this.#getScaleRatio(st);
|
|
2810
|
+
let x = st.start.x + m.dx / scale;
|
|
2811
|
+
let y = st.start.y + m.dy / scale;
|
|
2812
|
+
if (this.#opt.snapToGrid) {
|
|
2813
|
+
x = st.start.x + calcGrid(x - st.start.x, this.#opt.gridX);
|
|
2814
|
+
y = st.start.y + calcGrid(y - st.start.y, this.#opt.gridY);
|
|
2815
|
+
}
|
|
2816
|
+
st.data = {
|
|
2817
|
+
...st.data,
|
|
2818
|
+
x,
|
|
2819
|
+
y
|
|
2820
|
+
};
|
|
2821
|
+
this.#markline(st);
|
|
2822
|
+
this.#clampMove(st.data, st.boundary);
|
|
2823
|
+
}
|
|
2824
|
+
#computeRotate(m, st) {
|
|
2825
|
+
const scale = this.#getScaleRatio(st);
|
|
2826
|
+
const cRect = st.containerRect;
|
|
2827
|
+
if (!cRect) return;
|
|
2828
|
+
const mx = (m.x - cRect.left) / scale;
|
|
2829
|
+
const my = (m.y - cRect.top) / scale;
|
|
2830
|
+
let deg = angleToPointer(st.center.x, st.center.y, mx, my) - (st.rotateOffset ?? 0);
|
|
2831
|
+
deg = (deg % 360 + 360) % 360;
|
|
2832
|
+
if (this.#opt.rotateSnap) {
|
|
2833
|
+
const step = Number(this.#opt.rotateStep) || 15;
|
|
2834
|
+
deg = Math.round(deg / step) * step;
|
|
2835
|
+
}
|
|
2836
|
+
st.data = {
|
|
2837
|
+
...st.data,
|
|
2838
|
+
angle: deg
|
|
2839
|
+
};
|
|
2840
|
+
}
|
|
2841
|
+
#computeResize(m, st) {
|
|
2842
|
+
const scale = this.#getScaleRatio(st);
|
|
2843
|
+
const ratio = this.#opt.aspectRatio;
|
|
2844
|
+
const dx = m.dx / scale;
|
|
2845
|
+
const dy = m.dy / scale;
|
|
2846
|
+
const alpha = Math.atan2(dy, dx);
|
|
2847
|
+
const deltaL = getLength(dx, dy);
|
|
2848
|
+
const beta = alpha - toRad(st.start.angle);
|
|
2849
|
+
const deltaW = deltaL * Math.cos(beta);
|
|
2850
|
+
const deltaH = deltaL * Math.sin(beta);
|
|
2851
|
+
const rect = {
|
|
2852
|
+
centerX: st.start.x + st.start.width / 2,
|
|
2853
|
+
centerY: st.start.y + st.start.height / 2,
|
|
2854
|
+
height: st.start.height,
|
|
2855
|
+
rotateAngle: st.start.angle,
|
|
2856
|
+
width: st.start.width
|
|
2857
|
+
};
|
|
2858
|
+
const { position: { centerX, centerY }, size: { width: nw, height: nh } } = getNewStyle(st.side, rect, deltaW, deltaH, ratio, this.#opt.minWidth, this.#opt.minHeight);
|
|
2859
|
+
const tl = centerToTL({
|
|
2860
|
+
angle: st.data.angle,
|
|
2861
|
+
centerX,
|
|
2862
|
+
centerY,
|
|
2863
|
+
height: nh,
|
|
2864
|
+
width: nw
|
|
2865
|
+
});
|
|
2866
|
+
const next = {
|
|
2867
|
+
...st.data,
|
|
2868
|
+
...formatData(tl, centerX, centerY)
|
|
2869
|
+
};
|
|
2870
|
+
if (this.#opt.maxWidth > 0) next.width = Math.min(next.width, this.#opt.maxWidth);
|
|
2871
|
+
if (this.#opt.maxHeight > 0) next.height = Math.min(next.height, this.#opt.maxHeight);
|
|
2872
|
+
if (this.#opt.snapToGrid) {
|
|
2873
|
+
const cx = next.x + next.width / 2;
|
|
2874
|
+
const cy = next.y + next.height / 2;
|
|
2875
|
+
const gx = this.#opt.gridX;
|
|
2876
|
+
const gy = this.#opt.gridY;
|
|
2877
|
+
let w = st.start.width + calcGrid(next.width - st.start.width, gx);
|
|
2878
|
+
let h;
|
|
2879
|
+
if (ratio) h = w / ratio;
|
|
2880
|
+
else h = st.start.height + calcGrid(next.height - st.start.height, gy);
|
|
2881
|
+
next.width = Math.abs(w);
|
|
2882
|
+
next.height = Math.abs(h);
|
|
2883
|
+
next.x = cx - next.width / 2;
|
|
2884
|
+
next.y = cy - next.height / 2;
|
|
2885
|
+
}
|
|
2886
|
+
const tentativeData = next;
|
|
2887
|
+
if (this.#clampResize(tentativeData, st.boundary)) {
|
|
2888
|
+
st.data = tentativeData;
|
|
2889
|
+
st.lastValidData = { ...tentativeData };
|
|
2890
|
+
} else st.data = st.lastValidData ? { ...st.lastValidData } : { ...st.start };
|
|
2891
|
+
this.#marklineResize(st);
|
|
2892
|
+
}
|
|
2893
|
+
#calcViewportRect(st) {
|
|
2894
|
+
const d = st?.data;
|
|
2895
|
+
const off = st?.viewportCenterOffset;
|
|
2896
|
+
if (!d || !off) return st.startRect;
|
|
2897
|
+
const rad = toRad(d.angle || 0);
|
|
2898
|
+
const cos = Math.cos(rad);
|
|
2899
|
+
const sin = Math.sin(rad);
|
|
2900
|
+
const aabbW = Math.abs(d.width * cos) + Math.abs(d.height * sin);
|
|
2901
|
+
const aabbH = Math.abs(d.width * sin) + Math.abs(d.height * cos);
|
|
2902
|
+
const cx = d.x + d.width / 2 + off.x;
|
|
2903
|
+
const cy = d.y + d.height / 2 + off.y;
|
|
2904
|
+
const left = cx - aabbW / 2;
|
|
2905
|
+
const top = cy - aabbH / 2;
|
|
2906
|
+
return {
|
|
2907
|
+
bottom: top + aabbH,
|
|
2908
|
+
height: aabbH,
|
|
2909
|
+
left,
|
|
2910
|
+
right: left + aabbW,
|
|
2911
|
+
top,
|
|
2912
|
+
width: aabbW
|
|
2913
|
+
};
|
|
2914
|
+
}
|
|
2915
|
+
#markline(st) {
|
|
2916
|
+
if (!this.#opt.snap && !this.#opt.markline) return;
|
|
2917
|
+
if (!st?.lines || !st.containerRectScaled) return;
|
|
2918
|
+
const source = this.#calcViewportRect(st);
|
|
2919
|
+
const lines = st.lines || {
|
|
2920
|
+
x: [],
|
|
2921
|
+
y: []
|
|
2922
|
+
};
|
|
2923
|
+
const mark = {
|
|
2924
|
+
diffX: 0,
|
|
2925
|
+
diffY: 0,
|
|
2926
|
+
left: void 0,
|
|
2927
|
+
top: void 0
|
|
2928
|
+
};
|
|
2929
|
+
for (let i = 0; i < (lines.y.length || 0); i++) {
|
|
2930
|
+
const { top, showTop } = lines.y[i];
|
|
2931
|
+
const diff = top - source.top;
|
|
2932
|
+
if (Math.abs(diff) < this.#opt.snapThreshold) {
|
|
2933
|
+
mark.diffY = diff;
|
|
2934
|
+
mark.top = showTop - st.containerRectScaled.top;
|
|
2935
|
+
break;
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
for (let i = 0; i < (lines.x.length || 0); i++) {
|
|
2939
|
+
const { left, showLeft } = lines.x[i];
|
|
2940
|
+
const diff = left - source.left;
|
|
2941
|
+
if (Math.abs(diff) < this.#opt.snapThreshold) {
|
|
2942
|
+
mark.diffX = diff;
|
|
2943
|
+
mark.left = showLeft - st.containerRectScaled.left;
|
|
2944
|
+
break;
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
if (this.#opt.snap) {
|
|
2948
|
+
if (mark.diffX) st.data.x += mark.diffX;
|
|
2949
|
+
if (mark.diffY) st.data.y += mark.diffY;
|
|
2950
|
+
}
|
|
2951
|
+
st.mark = mark;
|
|
2952
|
+
}
|
|
2953
|
+
#marklineResize(st) {
|
|
2954
|
+
if (!this.#opt.snap && !this.#opt.markline) return;
|
|
2955
|
+
if (!st?.targetRects || !st.containerRectScaled) return;
|
|
2956
|
+
const mark = {
|
|
2957
|
+
diffX: 0,
|
|
2958
|
+
diffY: 0,
|
|
2959
|
+
left: void 0,
|
|
2960
|
+
top: void 0
|
|
2961
|
+
};
|
|
2962
|
+
if ((st.data.angle || 0) !== 0 || this.#opt.aspectRatio) {
|
|
2963
|
+
st.mark = mark;
|
|
2964
|
+
return;
|
|
2965
|
+
}
|
|
2966
|
+
const source = this.#calcViewportRect(st);
|
|
2967
|
+
const { diffX, diffY, guideLeft, guideTop } = resolveResizeSnap({
|
|
2968
|
+
side: st.side,
|
|
2969
|
+
source,
|
|
2970
|
+
targetRects: st.targetRects,
|
|
2971
|
+
threshold: this.#opt.snapThreshold
|
|
2972
|
+
});
|
|
2973
|
+
mark.diffX = diffX;
|
|
2974
|
+
mark.diffY = diffY;
|
|
2975
|
+
mark.left = guideLeft === void 0 ? void 0 : guideLeft - st.containerRectScaled.left;
|
|
2976
|
+
mark.top = guideTop === void 0 ? void 0 : guideTop - st.containerRectScaled.top;
|
|
2977
|
+
if (this.#opt.snap && (diffX || diffY)) {
|
|
2978
|
+
const scale = this.#getScaleRatio(st);
|
|
2979
|
+
const snapshot = { ...st.data };
|
|
2980
|
+
const next = { ...st.data };
|
|
2981
|
+
const side = st.side || "";
|
|
2982
|
+
if (diffX) {
|
|
2983
|
+
const dx = diffX / scale;
|
|
2984
|
+
if (side.includes("left")) {
|
|
2985
|
+
next.x += dx;
|
|
2986
|
+
next.width -= dx;
|
|
2987
|
+
} else if (side.includes("right")) next.width += dx;
|
|
2988
|
+
}
|
|
2989
|
+
if (diffY) {
|
|
2990
|
+
const dy = diffY / scale;
|
|
2991
|
+
if (side.includes("top")) {
|
|
2992
|
+
next.y += dy;
|
|
2993
|
+
next.height -= dy;
|
|
2994
|
+
} else if (side.includes("bottom")) next.height += dy;
|
|
2995
|
+
}
|
|
2996
|
+
let valid = next.width >= this.#opt.minWidth && next.height >= this.#opt.minHeight;
|
|
2997
|
+
if (valid && this.#opt.maxWidth > 0 && next.width > this.#opt.maxWidth) valid = false;
|
|
2998
|
+
if (valid && this.#opt.maxHeight > 0 && next.height > this.#opt.maxHeight) valid = false;
|
|
2999
|
+
if (valid && !this.#clampResize(next, st.boundary)) valid = false;
|
|
3000
|
+
if (valid) {
|
|
3001
|
+
st.data = next;
|
|
3002
|
+
st.lastValidData = { ...next };
|
|
3003
|
+
} else {
|
|
3004
|
+
st.data = snapshot;
|
|
3005
|
+
mark.left = void 0;
|
|
3006
|
+
mark.top = void 0;
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
st.mark = mark;
|
|
3010
|
+
}
|
|
3011
|
+
#commitMarkline(ctx, st) {
|
|
3012
|
+
const { guideX, guideY } = st;
|
|
3013
|
+
const mark = st.mark || {};
|
|
3014
|
+
if (guideX) if (mark.left === void 0) ctx.frame?.setStyle?.(guideX, "display", "none");
|
|
3015
|
+
else {
|
|
3016
|
+
ctx.frame?.setStyle?.(guideX, "display", "block");
|
|
3017
|
+
ctx.frame?.setStyle?.(guideX, "transform", `translate(${withUnit(mark.left)}, 0)`);
|
|
3018
|
+
}
|
|
3019
|
+
if (guideY) if (mark.top === void 0) ctx.frame?.setStyle?.(guideY, "display", "none");
|
|
3020
|
+
else {
|
|
3021
|
+
ctx.frame?.setStyle?.(guideY, "display", "block");
|
|
3022
|
+
ctx.frame?.setStyle?.(guideY, "transform", `translate(0, ${withUnit(mark.top)})`);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
#updateDotCursors(el, angle) {
|
|
3026
|
+
const dots = findDots(el);
|
|
3027
|
+
for (const dot of dots) {
|
|
3028
|
+
const dir = dot?.dataset?.dir;
|
|
3029
|
+
if (dir) dot.style.cursor = `${getCursor(angle, dir)}-resize`;
|
|
3030
|
+
}
|
|
3031
|
+
}
|
|
3032
|
+
#applyTransform(ctx, el, data) {
|
|
3033
|
+
ctx.frame?.setStyle?.(el, "transform", `translate(${withUnit(data.x)}, ${withUnit(data.y)}) rotate(${data.angle}deg)`);
|
|
3034
|
+
}
|
|
3035
|
+
#apply(ctx, el, st) {
|
|
3036
|
+
const d = st.data;
|
|
3037
|
+
if (st.lastAppliedW !== d.width) {
|
|
3038
|
+
ctx.frame?.setStyle?.(el, "width", withUnit(d.width));
|
|
3039
|
+
st.lastAppliedW = d.width;
|
|
3040
|
+
}
|
|
3041
|
+
if (st.lastAppliedH !== d.height) {
|
|
3042
|
+
ctx.frame?.setStyle?.(el, "height", withUnit(d.height));
|
|
3043
|
+
st.lastAppliedH = d.height;
|
|
3044
|
+
}
|
|
3045
|
+
if (st.lastAppliedX !== d.x || st.lastAppliedY !== d.y || st.lastAppliedAngle !== d.angle) {
|
|
3046
|
+
this.#applyTransform(ctx, el, d);
|
|
3047
|
+
st.lastAppliedX = d.x;
|
|
3048
|
+
st.lastAppliedY = d.y;
|
|
3049
|
+
st.lastAppliedAngle = d.angle;
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
#clampMove(d, boundary) {
|
|
3053
|
+
if (!boundary) return;
|
|
3054
|
+
const rad = toRad(d.angle || 0);
|
|
3055
|
+
const cos = Math.abs(Math.cos(rad));
|
|
3056
|
+
const sin = Math.abs(Math.sin(rad));
|
|
3057
|
+
const aabbW = d.width * cos + d.height * sin;
|
|
3058
|
+
const aabbH = d.width * sin + d.height * cos;
|
|
3059
|
+
const minX = (aabbW - d.width) / 2;
|
|
3060
|
+
const maxX = boundary.w - aabbW / 2 - d.width / 2;
|
|
3061
|
+
const minY = (aabbH - d.height) / 2;
|
|
3062
|
+
const maxY = boundary.h - aabbH / 2 - d.height / 2;
|
|
3063
|
+
d.x = Math.max(Math.min(minX, maxX), Math.min(d.x, Math.max(minX, maxX)));
|
|
3064
|
+
d.y = Math.max(Math.min(minY, maxY), Math.min(d.y, Math.max(minY, maxY)));
|
|
3065
|
+
}
|
|
3066
|
+
#clampResize(d, boundary) {
|
|
3067
|
+
if (!boundary) return true;
|
|
3068
|
+
const rad = toRad(d.angle || 0);
|
|
3069
|
+
const cos = Math.abs(Math.cos(rad));
|
|
3070
|
+
const sin = Math.abs(Math.sin(rad));
|
|
3071
|
+
const aabbW = d.width * cos + d.height * sin;
|
|
3072
|
+
const aabbH = d.width * sin + d.height * cos;
|
|
3073
|
+
const cx = d.x + d.width / 2;
|
|
3074
|
+
const cy = d.y + d.height / 2;
|
|
3075
|
+
const left = cx - aabbW / 2;
|
|
3076
|
+
const right = cx + aabbW / 2;
|
|
3077
|
+
const top = cy - aabbH / 2;
|
|
3078
|
+
const bottom = cy + aabbH / 2;
|
|
3079
|
+
const t = CLAMP_TOLERANCE;
|
|
3080
|
+
if (left < -t || top < -t || right > boundary.w + t || bottom > boundary.h + t) return false;
|
|
3081
|
+
return true;
|
|
3082
|
+
}
|
|
3083
|
+
#disableMirrorNow(m) {
|
|
3084
|
+
if (m?.mirrorEl) {
|
|
3085
|
+
try {
|
|
3086
|
+
m.mirrorEl.parentNode?.removeChild?.(m.mirrorEl);
|
|
3087
|
+
} catch {}
|
|
3088
|
+
m.mirrorEl = void 0;
|
|
3089
|
+
}
|
|
3090
|
+
if (isHTMLElement(m?.sourceEl)) try {
|
|
3091
|
+
m.sourceEl.style.visibility = "";
|
|
3092
|
+
} catch {}
|
|
3093
|
+
}
|
|
3094
|
+
#disableIndicatorNow(m) {
|
|
3095
|
+
if (m?.indicatorEl) {
|
|
3096
|
+
try {
|
|
3097
|
+
m.indicatorEl.parentNode?.removeChild?.(m.indicatorEl);
|
|
3098
|
+
} catch {}
|
|
3099
|
+
m.indicatorEl = void 0;
|
|
3100
|
+
m.indicatorRegion = void 0;
|
|
3101
|
+
}
|
|
3102
|
+
m.currentDrop = void 0;
|
|
3103
|
+
m.currentDropRect = void 0;
|
|
3104
|
+
m.currentAllowed = false;
|
|
3105
|
+
}
|
|
3106
|
+
};
|
|
3107
|
+
//#endregion
|
|
3108
|
+
//#region src/version.js
|
|
3109
|
+
var DndVersion = {
|
|
3110
|
+
name: "@ptahjs/dnd",
|
|
3111
|
+
version: "0.1.2"
|
|
3112
|
+
};
|
|
3113
|
+
//#endregion
|
|
3114
|
+
export { ACTIVE_WRAP_CLASS, ActiveSelectionService, AutoScrollService, DOT_CLASS, Dnd, DndVersion, DropIndicatorService, DropService, MarqueeSelectionService, MirrorService, ROTATE_CLASS, TransformControllerService, clearAll, setMultipleActive };
|