@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 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 };