@blorkfield/blork-tabs 0.1.3
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/LICENSE +21 -0
- package/README.md +166 -0
- package/dist/index.cjs +1194 -0
- package/dist/index.d.cts +669 -0
- package/dist/index.d.ts +669 -0
- package/dist/index.js +1143 -0
- package/dist/styles.css +186 -0
- package/package.json +62 -0
- package/src/AnchorManager.ts +395 -0
- package/src/DragManager.ts +251 -0
- package/src/Panel.ts +211 -0
- package/src/SnapChain.ts +289 -0
- package/src/SnapPreview.ts +91 -0
- package/src/TabManager.ts +507 -0
- package/src/index.test.ts +9 -0
- package/src/index.ts +105 -0
- package/src/types.ts +320 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1194 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
AnchorManager: () => AnchorManager,
|
|
24
|
+
DragManager: () => DragManager,
|
|
25
|
+
SnapPreview: () => SnapPreview,
|
|
26
|
+
TabManager: () => TabManager,
|
|
27
|
+
areInSameChain: () => areInSameChain,
|
|
28
|
+
createPanelElement: () => createPanelElement,
|
|
29
|
+
createPanelState: () => createPanelState,
|
|
30
|
+
createPresetAnchor: () => createPresetAnchor,
|
|
31
|
+
detachFromGroup: () => detachFromGroup,
|
|
32
|
+
findSnapTarget: () => findSnapTarget,
|
|
33
|
+
getConnectedGroup: () => getConnectedGroup,
|
|
34
|
+
getDefaultAnchorConfigs: () => getDefaultAnchorConfigs,
|
|
35
|
+
getDefaultZIndex: () => getDefaultZIndex,
|
|
36
|
+
getDragZIndex: () => getDragZIndex,
|
|
37
|
+
getLeftmostPanel: () => getLeftmostPanel,
|
|
38
|
+
getPanelDimensions: () => getPanelDimensions,
|
|
39
|
+
getPanelPosition: () => getPanelPosition,
|
|
40
|
+
getRightmostPanel: () => getRightmostPanel,
|
|
41
|
+
setPanelPosition: () => setPanelPosition,
|
|
42
|
+
setPanelZIndex: () => setPanelZIndex,
|
|
43
|
+
snapPanels: () => snapPanels,
|
|
44
|
+
snapPanelsToTarget: () => snapPanelsToTarget,
|
|
45
|
+
toggleCollapse: () => toggleCollapse,
|
|
46
|
+
unsnap: () => unsnap,
|
|
47
|
+
updateSnappedPositions: () => updateSnappedPositions
|
|
48
|
+
});
|
|
49
|
+
module.exports = __toCommonJS(index_exports);
|
|
50
|
+
|
|
51
|
+
// src/Panel.ts
|
|
52
|
+
function createPanelElement(config, classes) {
|
|
53
|
+
const element = document.createElement("div");
|
|
54
|
+
element.className = classes.panel;
|
|
55
|
+
element.id = `${config.id}-panel`;
|
|
56
|
+
element.style.width = `${config.width ?? 300}px`;
|
|
57
|
+
element.style.zIndex = `${config.zIndex ?? 1e3}`;
|
|
58
|
+
const header = document.createElement("div");
|
|
59
|
+
header.className = classes.panelHeader;
|
|
60
|
+
header.id = `${config.id}-header`;
|
|
61
|
+
let detachGrip = null;
|
|
62
|
+
if (config.detachable !== false) {
|
|
63
|
+
detachGrip = document.createElement("div");
|
|
64
|
+
detachGrip.className = classes.detachGrip;
|
|
65
|
+
detachGrip.id = `${config.id}-detach-grip`;
|
|
66
|
+
header.appendChild(detachGrip);
|
|
67
|
+
}
|
|
68
|
+
if (config.title) {
|
|
69
|
+
const title = document.createElement("span");
|
|
70
|
+
title.className = classes.panelTitle;
|
|
71
|
+
title.textContent = config.title;
|
|
72
|
+
header.appendChild(title);
|
|
73
|
+
}
|
|
74
|
+
let collapseButton = null;
|
|
75
|
+
if (config.collapsible !== false) {
|
|
76
|
+
collapseButton = document.createElement("button");
|
|
77
|
+
collapseButton.className = classes.collapseButton;
|
|
78
|
+
collapseButton.id = `${config.id}-collapse-btn`;
|
|
79
|
+
collapseButton.textContent = config.startCollapsed !== false ? "+" : "\u2212";
|
|
80
|
+
header.appendChild(collapseButton);
|
|
81
|
+
}
|
|
82
|
+
element.appendChild(header);
|
|
83
|
+
const contentWrapper = document.createElement("div");
|
|
84
|
+
contentWrapper.className = classes.panelContent;
|
|
85
|
+
contentWrapper.id = `${config.id}-content`;
|
|
86
|
+
if (config.startCollapsed !== false) {
|
|
87
|
+
contentWrapper.classList.add(classes.panelContentCollapsed);
|
|
88
|
+
}
|
|
89
|
+
if (config.content) {
|
|
90
|
+
if (typeof config.content === "string") {
|
|
91
|
+
contentWrapper.innerHTML = config.content;
|
|
92
|
+
} else {
|
|
93
|
+
contentWrapper.appendChild(config.content);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
element.appendChild(contentWrapper);
|
|
97
|
+
return {
|
|
98
|
+
element,
|
|
99
|
+
dragHandle: header,
|
|
100
|
+
collapseButton,
|
|
101
|
+
contentWrapper,
|
|
102
|
+
detachGrip
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
function createPanelState(config, classes) {
|
|
106
|
+
let element;
|
|
107
|
+
let dragHandle;
|
|
108
|
+
let collapseButton;
|
|
109
|
+
let contentWrapper;
|
|
110
|
+
let detachGrip;
|
|
111
|
+
if (config.element) {
|
|
112
|
+
element = config.element;
|
|
113
|
+
dragHandle = config.dragHandle ?? element.querySelector(`.${classes.panelHeader}`);
|
|
114
|
+
collapseButton = config.collapseButton ?? element.querySelector(`.${classes.collapseButton}`);
|
|
115
|
+
contentWrapper = config.contentWrapper ?? element.querySelector(`.${classes.panelContent}`);
|
|
116
|
+
detachGrip = config.detachGrip ?? element.querySelector(`.${classes.detachGrip}`);
|
|
117
|
+
} else {
|
|
118
|
+
const created = createPanelElement(config, classes);
|
|
119
|
+
element = created.element;
|
|
120
|
+
dragHandle = created.dragHandle;
|
|
121
|
+
collapseButton = created.collapseButton;
|
|
122
|
+
contentWrapper = created.contentWrapper;
|
|
123
|
+
detachGrip = created.detachGrip;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
id: config.id,
|
|
127
|
+
element,
|
|
128
|
+
dragHandle,
|
|
129
|
+
collapseButton,
|
|
130
|
+
contentWrapper,
|
|
131
|
+
detachGrip,
|
|
132
|
+
isCollapsed: config.startCollapsed !== false,
|
|
133
|
+
snappedTo: null,
|
|
134
|
+
snappedFrom: null,
|
|
135
|
+
config
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
function toggleCollapse(state, classes, collapsed) {
|
|
139
|
+
const newState = collapsed ?? !state.isCollapsed;
|
|
140
|
+
state.isCollapsed = newState;
|
|
141
|
+
if (newState) {
|
|
142
|
+
state.contentWrapper.classList.add(classes.panelContentCollapsed);
|
|
143
|
+
} else {
|
|
144
|
+
state.contentWrapper.classList.remove(classes.panelContentCollapsed);
|
|
145
|
+
}
|
|
146
|
+
if (state.collapseButton) {
|
|
147
|
+
state.collapseButton.textContent = newState ? "+" : "\u2212";
|
|
148
|
+
}
|
|
149
|
+
return newState;
|
|
150
|
+
}
|
|
151
|
+
function setPanelPosition(state, x, y) {
|
|
152
|
+
state.element.style.left = `${x}px`;
|
|
153
|
+
state.element.style.top = `${y}px`;
|
|
154
|
+
state.element.style.right = "auto";
|
|
155
|
+
}
|
|
156
|
+
function getPanelPosition(state) {
|
|
157
|
+
const rect = state.element.getBoundingClientRect();
|
|
158
|
+
return { x: rect.left, y: rect.top };
|
|
159
|
+
}
|
|
160
|
+
function getPanelDimensions(state) {
|
|
161
|
+
return {
|
|
162
|
+
width: state.element.offsetWidth,
|
|
163
|
+
height: state.element.offsetHeight
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
function setPanelZIndex(state, zIndex) {
|
|
167
|
+
state.element.style.zIndex = `${zIndex}`;
|
|
168
|
+
}
|
|
169
|
+
function getDefaultZIndex(state) {
|
|
170
|
+
return state.config.zIndex ?? 1e3;
|
|
171
|
+
}
|
|
172
|
+
function getDragZIndex(state) {
|
|
173
|
+
return state.config.dragZIndex ?? 1002;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// src/SnapChain.ts
|
|
177
|
+
function getConnectedGroup(startPanel, panels) {
|
|
178
|
+
const group = [];
|
|
179
|
+
const visited = /* @__PURE__ */ new Set();
|
|
180
|
+
let current = startPanel;
|
|
181
|
+
while (current && !visited.has(current.id)) {
|
|
182
|
+
visited.add(current.id);
|
|
183
|
+
group.unshift(current);
|
|
184
|
+
if (current.snappedFrom) {
|
|
185
|
+
current = panels.get(current.snappedFrom);
|
|
186
|
+
} else {
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
current = startPanel.snappedTo ? panels.get(startPanel.snappedTo) : void 0;
|
|
191
|
+
while (current && !visited.has(current.id)) {
|
|
192
|
+
visited.add(current.id);
|
|
193
|
+
group.push(current);
|
|
194
|
+
if (current.snappedTo) {
|
|
195
|
+
current = panels.get(current.snappedTo);
|
|
196
|
+
} else {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return group;
|
|
201
|
+
}
|
|
202
|
+
function detachFromGroup(panel, panels) {
|
|
203
|
+
if (panel.snappedTo) {
|
|
204
|
+
const rightPanel = panels.get(panel.snappedTo);
|
|
205
|
+
if (rightPanel) {
|
|
206
|
+
rightPanel.snappedFrom = null;
|
|
207
|
+
}
|
|
208
|
+
panel.snappedTo = null;
|
|
209
|
+
}
|
|
210
|
+
if (panel.snappedFrom) {
|
|
211
|
+
const leftPanel = panels.get(panel.snappedFrom);
|
|
212
|
+
if (leftPanel) {
|
|
213
|
+
leftPanel.snappedTo = null;
|
|
214
|
+
}
|
|
215
|
+
panel.snappedFrom = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function findSnapTarget(movingPanels, panels, config) {
|
|
219
|
+
if (movingPanels.length === 0) return null;
|
|
220
|
+
const leftmostPanel = movingPanels[0];
|
|
221
|
+
const leftmostRect = leftmostPanel.element.getBoundingClientRect();
|
|
222
|
+
let totalWidth = 0;
|
|
223
|
+
for (const p of movingPanels) {
|
|
224
|
+
totalWidth += p.element.offsetWidth + config.panelGap;
|
|
225
|
+
}
|
|
226
|
+
totalWidth -= config.panelGap;
|
|
227
|
+
const movingIds = new Set(movingPanels.map((p) => p.id));
|
|
228
|
+
const x = leftmostRect.left;
|
|
229
|
+
const y = leftmostRect.top;
|
|
230
|
+
for (const [id, targetState] of panels) {
|
|
231
|
+
if (movingIds.has(id)) continue;
|
|
232
|
+
const targetRect = targetState.element.getBoundingClientRect();
|
|
233
|
+
const verticalOverlap = Math.abs(y - targetRect.top) < config.snapThreshold * 2;
|
|
234
|
+
if (!verticalOverlap) continue;
|
|
235
|
+
const snapToLeftX = targetRect.left - totalWidth - config.panelGap;
|
|
236
|
+
if (Math.abs(x - snapToLeftX) < config.snapThreshold && !targetState.snappedFrom) {
|
|
237
|
+
return { targetId: id, side: "left", x: snapToLeftX, y: targetRect.top };
|
|
238
|
+
}
|
|
239
|
+
const snapToRightX = targetRect.right + config.panelGap;
|
|
240
|
+
if (Math.abs(x - snapToRightX) < config.snapThreshold && !targetState.snappedTo) {
|
|
241
|
+
return { targetId: id, side: "right", x: snapToRightX, y: targetRect.top };
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
function snapPanelsToTarget(movingPanels, targetId, side, x, y, panels, config) {
|
|
247
|
+
const targetState = panels.get(targetId);
|
|
248
|
+
if (!targetState) return;
|
|
249
|
+
const leftmostPanel = movingPanels[0];
|
|
250
|
+
const rightmostPanel = movingPanels[movingPanels.length - 1];
|
|
251
|
+
let currentX = x;
|
|
252
|
+
for (const p of movingPanels) {
|
|
253
|
+
p.element.style.left = `${currentX}px`;
|
|
254
|
+
p.element.style.top = `${y}px`;
|
|
255
|
+
currentX += p.element.offsetWidth + config.panelGap;
|
|
256
|
+
}
|
|
257
|
+
if (side === "left") {
|
|
258
|
+
rightmostPanel.snappedTo = targetId;
|
|
259
|
+
targetState.snappedFrom = rightmostPanel.id;
|
|
260
|
+
} else {
|
|
261
|
+
leftmostPanel.snappedFrom = targetId;
|
|
262
|
+
targetState.snappedTo = leftmostPanel.id;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function updateSnappedPositions(panels, config) {
|
|
266
|
+
let rightmost = null;
|
|
267
|
+
for (const state of panels.values()) {
|
|
268
|
+
if (state.snappedTo === null && state.snappedFrom !== null) {
|
|
269
|
+
rightmost = state;
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (!rightmost) {
|
|
274
|
+
for (const state of panels.values()) {
|
|
275
|
+
if (state.snappedFrom !== null) {
|
|
276
|
+
rightmost = state;
|
|
277
|
+
break;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (!rightmost) return;
|
|
282
|
+
let current = rightmost;
|
|
283
|
+
while (current && current.snappedFrom) {
|
|
284
|
+
const leftPanel = panels.get(current.snappedFrom);
|
|
285
|
+
if (!leftPanel) break;
|
|
286
|
+
const currentRect = current.element.getBoundingClientRect();
|
|
287
|
+
leftPanel.element.style.left = `${currentRect.left - leftPanel.element.offsetWidth - config.panelGap}px`;
|
|
288
|
+
leftPanel.element.style.top = `${currentRect.top}px`;
|
|
289
|
+
current = leftPanel;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function getLeftmostPanel(panel, panels) {
|
|
293
|
+
let current = panel;
|
|
294
|
+
while (current.snappedFrom) {
|
|
295
|
+
const left = panels.get(current.snappedFrom);
|
|
296
|
+
if (!left) break;
|
|
297
|
+
current = left;
|
|
298
|
+
}
|
|
299
|
+
return current;
|
|
300
|
+
}
|
|
301
|
+
function getRightmostPanel(panel, panels) {
|
|
302
|
+
let current = panel;
|
|
303
|
+
while (current.snappedTo) {
|
|
304
|
+
const right = panels.get(current.snappedTo);
|
|
305
|
+
if (!right) break;
|
|
306
|
+
current = right;
|
|
307
|
+
}
|
|
308
|
+
return current;
|
|
309
|
+
}
|
|
310
|
+
function areInSameChain(panel1, panel2, panels) {
|
|
311
|
+
const chain = getConnectedGroup(panel1, panels);
|
|
312
|
+
return chain.some((p) => p.id === panel2.id);
|
|
313
|
+
}
|
|
314
|
+
function snapPanels(leftPanel, rightPanel) {
|
|
315
|
+
leftPanel.snappedTo = rightPanel.id;
|
|
316
|
+
rightPanel.snappedFrom = leftPanel.id;
|
|
317
|
+
}
|
|
318
|
+
function unsnap(leftPanel, rightPanel) {
|
|
319
|
+
if (leftPanel.snappedTo === rightPanel.id) {
|
|
320
|
+
leftPanel.snappedTo = null;
|
|
321
|
+
}
|
|
322
|
+
if (rightPanel.snappedFrom === leftPanel.id) {
|
|
323
|
+
rightPanel.snappedFrom = null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// src/DragManager.ts
|
|
328
|
+
var DragManager = class {
|
|
329
|
+
constructor(panels, config, callbacks) {
|
|
330
|
+
this.activeDrag = null;
|
|
331
|
+
this.panels = panels;
|
|
332
|
+
this.config = config;
|
|
333
|
+
this.callbacks = callbacks;
|
|
334
|
+
this.boundMouseMove = this.handleMouseMove.bind(this);
|
|
335
|
+
this.boundMouseUp = this.handleMouseUp.bind(this);
|
|
336
|
+
document.addEventListener("mousemove", this.boundMouseMove);
|
|
337
|
+
document.addEventListener("mouseup", this.boundMouseUp);
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Start a drag operation
|
|
341
|
+
*/
|
|
342
|
+
startDrag(e, panel, mode) {
|
|
343
|
+
e.preventDefault();
|
|
344
|
+
e.stopPropagation();
|
|
345
|
+
const connectedPanels = getConnectedGroup(panel, this.panels);
|
|
346
|
+
const initialGroupPositions = /* @__PURE__ */ new Map();
|
|
347
|
+
for (const p of connectedPanels) {
|
|
348
|
+
const rect2 = p.element.getBoundingClientRect();
|
|
349
|
+
initialGroupPositions.set(p.id, { x: rect2.left, y: rect2.top });
|
|
350
|
+
}
|
|
351
|
+
let movingPanels;
|
|
352
|
+
if (mode === "single") {
|
|
353
|
+
detachFromGroup(panel, this.panels);
|
|
354
|
+
movingPanels = [panel];
|
|
355
|
+
} else {
|
|
356
|
+
movingPanels = connectedPanels;
|
|
357
|
+
}
|
|
358
|
+
const rect = panel.element.getBoundingClientRect();
|
|
359
|
+
this.activeDrag = {
|
|
360
|
+
grabbedPanel: panel,
|
|
361
|
+
offsetX: e.clientX - rect.left,
|
|
362
|
+
offsetY: e.clientY - rect.top,
|
|
363
|
+
initialGroupPositions,
|
|
364
|
+
movingPanels,
|
|
365
|
+
mode
|
|
366
|
+
};
|
|
367
|
+
for (const p of movingPanels) {
|
|
368
|
+
setPanelZIndex(p, getDragZIndex(p));
|
|
369
|
+
}
|
|
370
|
+
document.body.style.userSelect = "none";
|
|
371
|
+
this.callbacks.onDragStart?.(this.activeDrag);
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Handle mouse movement during drag
|
|
375
|
+
*/
|
|
376
|
+
handleMouseMove(e) {
|
|
377
|
+
if (!this.activeDrag) return;
|
|
378
|
+
const { grabbedPanel, movingPanels, initialGroupPositions, mode } = this.activeDrag;
|
|
379
|
+
const panel = grabbedPanel.element;
|
|
380
|
+
const x = e.clientX - this.activeDrag.offsetX;
|
|
381
|
+
const y = e.clientY - this.activeDrag.offsetY;
|
|
382
|
+
const maxX = window.innerWidth - panel.offsetWidth;
|
|
383
|
+
const maxY = window.innerHeight - panel.offsetHeight;
|
|
384
|
+
const clampedX = Math.max(0, Math.min(x, maxX));
|
|
385
|
+
const clampedY = Math.max(0, Math.min(y, maxY));
|
|
386
|
+
panel.style.left = `${clampedX}px`;
|
|
387
|
+
panel.style.top = `${clampedY}px`;
|
|
388
|
+
if (mode === "group" && movingPanels.length > 1) {
|
|
389
|
+
const grabbedInitialPos = initialGroupPositions.get(grabbedPanel.id);
|
|
390
|
+
const deltaX = clampedX - grabbedInitialPos.x;
|
|
391
|
+
const deltaY = clampedY - grabbedInitialPos.y;
|
|
392
|
+
for (const p of movingPanels) {
|
|
393
|
+
if (p === grabbedPanel) continue;
|
|
394
|
+
const initialPos = initialGroupPositions.get(p.id);
|
|
395
|
+
const newX = Math.max(
|
|
396
|
+
0,
|
|
397
|
+
Math.min(initialPos.x + deltaX, window.innerWidth - p.element.offsetWidth)
|
|
398
|
+
);
|
|
399
|
+
const newY = Math.max(
|
|
400
|
+
0,
|
|
401
|
+
Math.min(initialPos.y + deltaY, window.innerHeight - p.element.offsetHeight)
|
|
402
|
+
);
|
|
403
|
+
p.element.style.left = `${newX}px`;
|
|
404
|
+
p.element.style.top = `${newY}px`;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const snapTarget = findSnapTarget(movingPanels, this.panels, this.config);
|
|
408
|
+
const anchorResult = snapTarget ? null : this.callbacks.findAnchorTarget?.(movingPanels) ?? null;
|
|
409
|
+
this.callbacks.onDragMove?.(
|
|
410
|
+
this.activeDrag,
|
|
411
|
+
{ x: clampedX, y: clampedY },
|
|
412
|
+
snapTarget,
|
|
413
|
+
anchorResult
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Handle mouse up - finalize drag
|
|
418
|
+
*/
|
|
419
|
+
handleMouseUp(_e) {
|
|
420
|
+
if (!this.activeDrag) return;
|
|
421
|
+
const { movingPanels } = this.activeDrag;
|
|
422
|
+
const snapTarget = findSnapTarget(movingPanels, this.panels, this.config);
|
|
423
|
+
let anchorResult = null;
|
|
424
|
+
if (snapTarget) {
|
|
425
|
+
snapPanelsToTarget(
|
|
426
|
+
movingPanels,
|
|
427
|
+
snapTarget.targetId,
|
|
428
|
+
snapTarget.side,
|
|
429
|
+
snapTarget.x,
|
|
430
|
+
snapTarget.y,
|
|
431
|
+
this.panels,
|
|
432
|
+
this.config
|
|
433
|
+
);
|
|
434
|
+
} else {
|
|
435
|
+
anchorResult = this.callbacks.findAnchorTarget?.(movingPanels) ?? null;
|
|
436
|
+
if (anchorResult) {
|
|
437
|
+
for (let i = 0; i < movingPanels.length; i++) {
|
|
438
|
+
movingPanels[i].element.style.left = `${anchorResult.positions[i].x}px`;
|
|
439
|
+
movingPanels[i].element.style.top = `${anchorResult.positions[i].y}px`;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const p of movingPanels) {
|
|
444
|
+
setPanelZIndex(p, getDefaultZIndex(p));
|
|
445
|
+
}
|
|
446
|
+
document.body.style.userSelect = "";
|
|
447
|
+
this.callbacks.onDragEnd?.(this.activeDrag, snapTarget, anchorResult);
|
|
448
|
+
this.activeDrag = null;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Check if a drag is currently in progress
|
|
452
|
+
*/
|
|
453
|
+
isActive() {
|
|
454
|
+
return this.activeDrag !== null;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Get the current drag state
|
|
458
|
+
*/
|
|
459
|
+
getState() {
|
|
460
|
+
return this.activeDrag;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* Clean up event listeners
|
|
464
|
+
*/
|
|
465
|
+
destroy() {
|
|
466
|
+
document.removeEventListener("mousemove", this.boundMouseMove);
|
|
467
|
+
document.removeEventListener("mouseup", this.boundMouseUp);
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
|
|
471
|
+
// src/AnchorManager.ts
|
|
472
|
+
function getDefaultAnchorConfigs(config) {
|
|
473
|
+
const { panelMargin, defaultPanelWidth } = config;
|
|
474
|
+
return [
|
|
475
|
+
{
|
|
476
|
+
id: "top-left",
|
|
477
|
+
getPosition: () => ({ x: panelMargin, y: panelMargin })
|
|
478
|
+
},
|
|
479
|
+
{
|
|
480
|
+
id: "top-right",
|
|
481
|
+
getPosition: () => ({
|
|
482
|
+
x: window.innerWidth - panelMargin - defaultPanelWidth,
|
|
483
|
+
y: panelMargin
|
|
484
|
+
})
|
|
485
|
+
},
|
|
486
|
+
{
|
|
487
|
+
id: "bottom-left",
|
|
488
|
+
getPosition: () => ({
|
|
489
|
+
x: panelMargin,
|
|
490
|
+
y: window.innerHeight - panelMargin
|
|
491
|
+
})
|
|
492
|
+
},
|
|
493
|
+
{
|
|
494
|
+
id: "bottom-right",
|
|
495
|
+
getPosition: () => ({
|
|
496
|
+
x: window.innerWidth - panelMargin - defaultPanelWidth,
|
|
497
|
+
y: window.innerHeight - panelMargin
|
|
498
|
+
})
|
|
499
|
+
},
|
|
500
|
+
{
|
|
501
|
+
id: "top-center",
|
|
502
|
+
getPosition: () => ({
|
|
503
|
+
x: (window.innerWidth - defaultPanelWidth) / 2,
|
|
504
|
+
y: panelMargin
|
|
505
|
+
})
|
|
506
|
+
}
|
|
507
|
+
];
|
|
508
|
+
}
|
|
509
|
+
function createPresetAnchor(preset, config) {
|
|
510
|
+
const { panelMargin, defaultPanelWidth } = config;
|
|
511
|
+
const presets = {
|
|
512
|
+
"top-left": () => ({ x: panelMargin, y: panelMargin }),
|
|
513
|
+
"top-right": () => ({
|
|
514
|
+
x: window.innerWidth - panelMargin - defaultPanelWidth,
|
|
515
|
+
y: panelMargin
|
|
516
|
+
}),
|
|
517
|
+
"top-center": () => ({
|
|
518
|
+
x: (window.innerWidth - defaultPanelWidth) / 2,
|
|
519
|
+
y: panelMargin
|
|
520
|
+
}),
|
|
521
|
+
"bottom-left": () => ({
|
|
522
|
+
x: panelMargin,
|
|
523
|
+
y: window.innerHeight - panelMargin
|
|
524
|
+
}),
|
|
525
|
+
"bottom-right": () => ({
|
|
526
|
+
x: window.innerWidth - panelMargin - defaultPanelWidth,
|
|
527
|
+
y: window.innerHeight - panelMargin
|
|
528
|
+
}),
|
|
529
|
+
"bottom-center": () => ({
|
|
530
|
+
x: (window.innerWidth - defaultPanelWidth) / 2,
|
|
531
|
+
y: window.innerHeight - panelMargin
|
|
532
|
+
}),
|
|
533
|
+
"center-left": () => ({
|
|
534
|
+
x: panelMargin,
|
|
535
|
+
y: window.innerHeight / 2
|
|
536
|
+
}),
|
|
537
|
+
"center-right": () => ({
|
|
538
|
+
x: window.innerWidth - panelMargin - defaultPanelWidth,
|
|
539
|
+
y: window.innerHeight / 2
|
|
540
|
+
})
|
|
541
|
+
};
|
|
542
|
+
return {
|
|
543
|
+
id: preset,
|
|
544
|
+
getPosition: presets[preset]
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
var AnchorManager = class {
|
|
548
|
+
constructor(config, classes) {
|
|
549
|
+
this.anchors = [];
|
|
550
|
+
this.config = config;
|
|
551
|
+
this.classes = classes;
|
|
552
|
+
this.container = config.container;
|
|
553
|
+
window.addEventListener("resize", this.updateIndicatorPositions.bind(this));
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Create an anchor indicator element
|
|
557
|
+
*/
|
|
558
|
+
createIndicator() {
|
|
559
|
+
const indicator = document.createElement("div");
|
|
560
|
+
indicator.className = this.classes.anchorIndicator;
|
|
561
|
+
this.container.appendChild(indicator);
|
|
562
|
+
return indicator;
|
|
563
|
+
}
|
|
564
|
+
/**
|
|
565
|
+
* Add an anchor point
|
|
566
|
+
*/
|
|
567
|
+
addAnchor(anchorConfig) {
|
|
568
|
+
const indicator = anchorConfig.showIndicator !== false ? this.createIndicator() : null;
|
|
569
|
+
const state = {
|
|
570
|
+
config: anchorConfig,
|
|
571
|
+
indicator
|
|
572
|
+
};
|
|
573
|
+
this.anchors.push(state);
|
|
574
|
+
this.updateIndicatorPosition(state);
|
|
575
|
+
return state;
|
|
576
|
+
}
|
|
577
|
+
/**
|
|
578
|
+
* Add multiple default anchors
|
|
579
|
+
*/
|
|
580
|
+
addDefaultAnchors() {
|
|
581
|
+
const defaults = getDefaultAnchorConfigs(this.config);
|
|
582
|
+
for (const config of defaults) {
|
|
583
|
+
this.addAnchor(config);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Add anchor from preset
|
|
588
|
+
*/
|
|
589
|
+
addPresetAnchor(preset) {
|
|
590
|
+
return this.addAnchor(createPresetAnchor(preset, this.config));
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Remove an anchor
|
|
594
|
+
*/
|
|
595
|
+
removeAnchor(id) {
|
|
596
|
+
const index = this.anchors.findIndex((a) => a.config.id === id);
|
|
597
|
+
if (index === -1) return false;
|
|
598
|
+
const anchor = this.anchors[index];
|
|
599
|
+
anchor.indicator?.remove();
|
|
600
|
+
this.anchors.splice(index, 1);
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
/**
|
|
604
|
+
* Update position of a single indicator
|
|
605
|
+
*/
|
|
606
|
+
updateIndicatorPosition(anchor) {
|
|
607
|
+
if (!anchor.indicator) return;
|
|
608
|
+
const pos = anchor.config.getPosition();
|
|
609
|
+
const indicator = anchor.indicator;
|
|
610
|
+
indicator.style.left = `${pos.x - 20}px`;
|
|
611
|
+
if (anchor.config.id.includes("bottom")) {
|
|
612
|
+
indicator.style.top = `${pos.y - 40}px`;
|
|
613
|
+
} else {
|
|
614
|
+
indicator.style.top = `${pos.y}px`;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
/**
|
|
618
|
+
* Update all indicator positions (call on window resize)
|
|
619
|
+
*/
|
|
620
|
+
updateIndicatorPositions() {
|
|
621
|
+
for (const anchor of this.anchors) {
|
|
622
|
+
this.updateIndicatorPosition(anchor);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
/**
|
|
626
|
+
* Find the nearest anchor for a group of moving panels
|
|
627
|
+
*/
|
|
628
|
+
findNearestAnchor(movingPanels) {
|
|
629
|
+
if (movingPanels.length === 0 || this.anchors.length === 0) return null;
|
|
630
|
+
let bestResult = null;
|
|
631
|
+
let bestDist = Infinity;
|
|
632
|
+
const firstRect = movingPanels[0].element.getBoundingClientRect();
|
|
633
|
+
const lastRect = movingPanels[movingPanels.length - 1].element.getBoundingClientRect();
|
|
634
|
+
const groupLeft = firstRect.left;
|
|
635
|
+
const groupRight = lastRect.right;
|
|
636
|
+
const groupTop = firstRect.top;
|
|
637
|
+
const groupBottom = firstRect.bottom;
|
|
638
|
+
for (const anchor of this.anchors) {
|
|
639
|
+
const anchorPos = anchor.config.getPosition();
|
|
640
|
+
const nearGroup = anchorPos.x >= groupLeft - this.config.anchorThreshold && anchorPos.x <= groupRight + this.config.anchorThreshold && anchorPos.y >= groupTop - this.config.anchorThreshold && anchorPos.y <= groupBottom + this.config.anchorThreshold;
|
|
641
|
+
if (!nearGroup) continue;
|
|
642
|
+
let closestPanelIdx = 0;
|
|
643
|
+
let closestDist = Infinity;
|
|
644
|
+
for (let i = 0; i < movingPanels.length; i++) {
|
|
645
|
+
const rect = movingPanels[i].element.getBoundingClientRect();
|
|
646
|
+
const gripX = rect.left;
|
|
647
|
+
const gripY = rect.top;
|
|
648
|
+
const dist = Math.sqrt(
|
|
649
|
+
Math.pow(gripX - anchorPos.x, 2) + Math.pow(gripY - anchorPos.y, 2)
|
|
650
|
+
);
|
|
651
|
+
if (dist < closestDist) {
|
|
652
|
+
closestDist = dist;
|
|
653
|
+
closestPanelIdx = i;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
const anchorX = anchorPos.x;
|
|
657
|
+
const anchorY = anchor.config.id.includes("bottom") ? anchorPos.y - movingPanels[closestPanelIdx].element.offsetHeight : anchorPos.y;
|
|
658
|
+
const positions = [];
|
|
659
|
+
let leftX = anchorX;
|
|
660
|
+
for (let i = closestPanelIdx - 1; i >= 0; i--) {
|
|
661
|
+
leftX -= movingPanels[i].element.offsetWidth + this.config.panelGap;
|
|
662
|
+
}
|
|
663
|
+
let currentX = leftX;
|
|
664
|
+
for (let i = 0; i < movingPanels.length; i++) {
|
|
665
|
+
positions.push({ x: currentX, y: anchorY });
|
|
666
|
+
currentX += movingPanels[i].element.offsetWidth + this.config.panelGap;
|
|
667
|
+
}
|
|
668
|
+
const leftmostX = positions[0].x;
|
|
669
|
+
const rightmostX = positions[positions.length - 1].x + movingPanels[movingPanels.length - 1].element.offsetWidth;
|
|
670
|
+
if (leftmostX < 0 || rightmostX > window.innerWidth) {
|
|
671
|
+
for (let tryIdx = 0; tryIdx < movingPanels.length; tryIdx++) {
|
|
672
|
+
const tryPositions = [];
|
|
673
|
+
let tryLeftX = anchorX;
|
|
674
|
+
for (let i = tryIdx - 1; i >= 0; i--) {
|
|
675
|
+
tryLeftX -= movingPanels[i].element.offsetWidth + this.config.panelGap;
|
|
676
|
+
}
|
|
677
|
+
let tryCurrentX = tryLeftX;
|
|
678
|
+
for (let i = 0; i < movingPanels.length; i++) {
|
|
679
|
+
tryPositions.push({ x: tryCurrentX, y: anchorY });
|
|
680
|
+
tryCurrentX += movingPanels[i].element.offsetWidth + this.config.panelGap;
|
|
681
|
+
}
|
|
682
|
+
const tryLeftmostX = tryPositions[0].x;
|
|
683
|
+
const tryRightmostX = tryPositions[tryPositions.length - 1].x + movingPanels[movingPanels.length - 1].element.offsetWidth;
|
|
684
|
+
if (tryLeftmostX >= 0 && tryRightmostX <= window.innerWidth) {
|
|
685
|
+
if (closestDist < bestDist) {
|
|
686
|
+
bestDist = closestDist;
|
|
687
|
+
bestResult = {
|
|
688
|
+
anchor,
|
|
689
|
+
dockPanelIndex: tryIdx,
|
|
690
|
+
positions: tryPositions
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
continue;
|
|
697
|
+
}
|
|
698
|
+
if (closestDist < bestDist) {
|
|
699
|
+
bestDist = closestDist;
|
|
700
|
+
bestResult = { anchor, dockPanelIndex: closestPanelIdx, positions };
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
return bestResult;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Show anchor indicators during drag
|
|
707
|
+
*/
|
|
708
|
+
showIndicators(activeAnchor) {
|
|
709
|
+
for (const anchor of this.anchors) {
|
|
710
|
+
if (!anchor.indicator) continue;
|
|
711
|
+
if (activeAnchor === anchor) {
|
|
712
|
+
anchor.indicator.classList.add(
|
|
713
|
+
this.classes.anchorIndicatorVisible,
|
|
714
|
+
this.classes.anchorIndicatorActive
|
|
715
|
+
);
|
|
716
|
+
} else {
|
|
717
|
+
anchor.indicator.classList.add(this.classes.anchorIndicatorVisible);
|
|
718
|
+
anchor.indicator.classList.remove(this.classes.anchorIndicatorActive);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Hide all anchor indicators
|
|
724
|
+
*/
|
|
725
|
+
hideIndicators() {
|
|
726
|
+
for (const anchor of this.anchors) {
|
|
727
|
+
if (!anchor.indicator) continue;
|
|
728
|
+
anchor.indicator.classList.remove(
|
|
729
|
+
this.classes.anchorIndicatorVisible,
|
|
730
|
+
this.classes.anchorIndicatorActive
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
/**
|
|
735
|
+
* Get all anchors
|
|
736
|
+
*/
|
|
737
|
+
getAnchors() {
|
|
738
|
+
return [...this.anchors];
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Get anchor by ID
|
|
742
|
+
*/
|
|
743
|
+
getAnchor(id) {
|
|
744
|
+
return this.anchors.find((a) => a.config.id === id);
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Clean up
|
|
748
|
+
*/
|
|
749
|
+
destroy() {
|
|
750
|
+
window.removeEventListener("resize", this.updateIndicatorPositions.bind(this));
|
|
751
|
+
for (const anchor of this.anchors) {
|
|
752
|
+
anchor.indicator?.remove();
|
|
753
|
+
}
|
|
754
|
+
this.anchors = [];
|
|
755
|
+
}
|
|
756
|
+
};
|
|
757
|
+
|
|
758
|
+
// src/SnapPreview.ts
|
|
759
|
+
var SnapPreview = class {
|
|
760
|
+
constructor(container, classes, panels) {
|
|
761
|
+
this.element = null;
|
|
762
|
+
this.container = container;
|
|
763
|
+
this.classes = classes;
|
|
764
|
+
this.panels = panels;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Create the preview element lazily
|
|
768
|
+
*/
|
|
769
|
+
ensureElement() {
|
|
770
|
+
if (!this.element) {
|
|
771
|
+
this.element = document.createElement("div");
|
|
772
|
+
this.element.className = this.classes.snapPreview;
|
|
773
|
+
this.container.appendChild(this.element);
|
|
774
|
+
}
|
|
775
|
+
return this.element;
|
|
776
|
+
}
|
|
777
|
+
/**
|
|
778
|
+
* Update the preview to show a snap target
|
|
779
|
+
*/
|
|
780
|
+
show(snapTarget) {
|
|
781
|
+
const preview = this.ensureElement();
|
|
782
|
+
const targetState = this.panels.get(snapTarget.targetId);
|
|
783
|
+
if (!targetState) {
|
|
784
|
+
this.hide();
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
const targetRect = targetState.element.getBoundingClientRect();
|
|
788
|
+
preview.style.top = `${targetRect.top}px`;
|
|
789
|
+
preview.style.height = `${targetRect.height}px`;
|
|
790
|
+
preview.style.left = `${snapTarget.side === "left" ? targetRect.left - 2 : targetRect.right - 2}px`;
|
|
791
|
+
preview.classList.add(this.classes.snapPreviewVisible);
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Hide the preview
|
|
795
|
+
*/
|
|
796
|
+
hide() {
|
|
797
|
+
if (this.element) {
|
|
798
|
+
this.element.classList.remove(this.classes.snapPreviewVisible);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
/**
|
|
802
|
+
* Update based on current snap target (convenience method)
|
|
803
|
+
*/
|
|
804
|
+
update(snapTarget) {
|
|
805
|
+
if (snapTarget) {
|
|
806
|
+
this.show(snapTarget);
|
|
807
|
+
} else {
|
|
808
|
+
this.hide();
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
/**
|
|
812
|
+
* Clean up
|
|
813
|
+
*/
|
|
814
|
+
destroy() {
|
|
815
|
+
this.element?.remove();
|
|
816
|
+
this.element = null;
|
|
817
|
+
}
|
|
818
|
+
};
|
|
819
|
+
|
|
820
|
+
// src/TabManager.ts
|
|
821
|
+
var DEFAULT_CONFIG = {
|
|
822
|
+
snapThreshold: 50,
|
|
823
|
+
panelGap: 0,
|
|
824
|
+
panelMargin: 16,
|
|
825
|
+
anchorThreshold: 80,
|
|
826
|
+
defaultPanelWidth: 300,
|
|
827
|
+
container: document.body,
|
|
828
|
+
initializeDefaultAnchors: true,
|
|
829
|
+
classPrefix: "blork-tabs"
|
|
830
|
+
};
|
|
831
|
+
function generateClasses(prefix) {
|
|
832
|
+
return {
|
|
833
|
+
panel: `${prefix}-panel`,
|
|
834
|
+
panelHeader: `${prefix}-header`,
|
|
835
|
+
panelTitle: `${prefix}-title`,
|
|
836
|
+
panelContent: `${prefix}-content`,
|
|
837
|
+
panelContentCollapsed: `${prefix}-content-collapsed`,
|
|
838
|
+
detachGrip: `${prefix}-detach-grip`,
|
|
839
|
+
collapseButton: `${prefix}-collapse-btn`,
|
|
840
|
+
snapPreview: `${prefix}-snap-preview`,
|
|
841
|
+
snapPreviewVisible: `${prefix}-snap-preview-visible`,
|
|
842
|
+
anchorIndicator: `${prefix}-anchor-indicator`,
|
|
843
|
+
anchorIndicatorVisible: `${prefix}-anchor-indicator-visible`,
|
|
844
|
+
anchorIndicatorActive: `${prefix}-anchor-indicator-active`,
|
|
845
|
+
dragging: `${prefix}-dragging`
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
var TabManager = class {
|
|
849
|
+
constructor(userConfig = {}) {
|
|
850
|
+
this.panels = /* @__PURE__ */ new Map();
|
|
851
|
+
this.eventListeners = /* @__PURE__ */ new Map();
|
|
852
|
+
this.config = {
|
|
853
|
+
...DEFAULT_CONFIG,
|
|
854
|
+
...userConfig,
|
|
855
|
+
container: userConfig.container ?? document.body
|
|
856
|
+
};
|
|
857
|
+
this.classes = generateClasses(this.config.classPrefix);
|
|
858
|
+
this.anchorManager = new AnchorManager(this.config, this.classes);
|
|
859
|
+
this.snapPreview = new SnapPreview(
|
|
860
|
+
this.config.container,
|
|
861
|
+
this.classes,
|
|
862
|
+
this.panels
|
|
863
|
+
);
|
|
864
|
+
this.dragManager = new DragManager(
|
|
865
|
+
this.panels,
|
|
866
|
+
this.config,
|
|
867
|
+
{
|
|
868
|
+
onDragStart: this.handleDragStart.bind(this),
|
|
869
|
+
onDragMove: this.handleDragMove.bind(this),
|
|
870
|
+
onDragEnd: this.handleDragEnd.bind(this),
|
|
871
|
+
findAnchorTarget: (panels) => this.anchorManager.findNearestAnchor(panels)
|
|
872
|
+
}
|
|
873
|
+
);
|
|
874
|
+
if (this.config.initializeDefaultAnchors) {
|
|
875
|
+
this.anchorManager.addDefaultAnchors();
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
// ==================== Panel Management ====================
|
|
879
|
+
/**
|
|
880
|
+
* Add a new panel
|
|
881
|
+
*/
|
|
882
|
+
addPanel(panelConfig) {
|
|
883
|
+
const state = createPanelState(panelConfig, this.classes);
|
|
884
|
+
if (!panelConfig.element && !this.config.container.contains(state.element)) {
|
|
885
|
+
this.config.container.appendChild(state.element);
|
|
886
|
+
}
|
|
887
|
+
this.setupPanelEvents(state);
|
|
888
|
+
this.panels.set(state.id, state);
|
|
889
|
+
if (panelConfig.initialPosition) {
|
|
890
|
+
setPanelPosition(state, panelConfig.initialPosition.x, panelConfig.initialPosition.y);
|
|
891
|
+
}
|
|
892
|
+
this.emit("panel:added", { panel: state });
|
|
893
|
+
return state;
|
|
894
|
+
}
|
|
895
|
+
/**
|
|
896
|
+
* Register an existing panel element
|
|
897
|
+
*/
|
|
898
|
+
registerPanel(id, element, options = {}) {
|
|
899
|
+
return this.addPanel({
|
|
900
|
+
id,
|
|
901
|
+
element,
|
|
902
|
+
dragHandle: options.dragHandle,
|
|
903
|
+
collapseButton: options.collapseButton,
|
|
904
|
+
contentWrapper: options.contentWrapper,
|
|
905
|
+
detachGrip: options.detachGrip,
|
|
906
|
+
startCollapsed: options.startCollapsed
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
/**
|
|
910
|
+
* Remove a panel
|
|
911
|
+
*/
|
|
912
|
+
removePanel(id) {
|
|
913
|
+
const panel = this.panels.get(id);
|
|
914
|
+
if (!panel) return false;
|
|
915
|
+
detachFromGroup(panel, this.panels);
|
|
916
|
+
if (!panel.config.element) {
|
|
917
|
+
panel.element.remove();
|
|
918
|
+
}
|
|
919
|
+
this.panels.delete(id);
|
|
920
|
+
this.emit("panel:removed", { panelId: id });
|
|
921
|
+
return true;
|
|
922
|
+
}
|
|
923
|
+
/**
|
|
924
|
+
* Get a panel by ID
|
|
925
|
+
*/
|
|
926
|
+
getPanel(id) {
|
|
927
|
+
return this.panels.get(id);
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Get all panels
|
|
931
|
+
*/
|
|
932
|
+
getAllPanels() {
|
|
933
|
+
return Array.from(this.panels.values());
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Set up event handlers for a panel
|
|
937
|
+
*/
|
|
938
|
+
setupPanelEvents(state) {
|
|
939
|
+
if (state.collapseButton) {
|
|
940
|
+
state.collapseButton.addEventListener("click", () => {
|
|
941
|
+
const newState = toggleCollapse(state, this.classes);
|
|
942
|
+
updateSnappedPositions(this.panels, this.config);
|
|
943
|
+
this.emit("panel:collapse", { panel: state, isCollapsed: newState });
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
if (state.detachGrip) {
|
|
947
|
+
state.detachGrip.addEventListener("mousedown", (e) => {
|
|
948
|
+
this.dragManager.startDrag(e, state, "single");
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
state.dragHandle.addEventListener("mousedown", (e) => {
|
|
952
|
+
if (e.target === state.collapseButton || e.target === state.detachGrip) {
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
955
|
+
this.dragManager.startDrag(e, state, "group");
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
// ==================== Snap Chain Management ====================
|
|
959
|
+
/**
|
|
960
|
+
* Get all panels in the same snap chain as the given panel
|
|
961
|
+
*/
|
|
962
|
+
getSnapChain(panelId) {
|
|
963
|
+
const panel = this.panels.get(panelId);
|
|
964
|
+
if (!panel) return [];
|
|
965
|
+
return getConnectedGroup(panel, this.panels);
|
|
966
|
+
}
|
|
967
|
+
/**
|
|
968
|
+
* Manually snap two panels together
|
|
969
|
+
*/
|
|
970
|
+
snap(leftPanelId, rightPanelId) {
|
|
971
|
+
const leftPanel = this.panels.get(leftPanelId);
|
|
972
|
+
const rightPanel = this.panels.get(rightPanelId);
|
|
973
|
+
if (!leftPanel || !rightPanel) return false;
|
|
974
|
+
snapPanels(leftPanel, rightPanel);
|
|
975
|
+
updateSnappedPositions(this.panels, this.config);
|
|
976
|
+
return true;
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Detach a panel from its snap chain
|
|
980
|
+
*/
|
|
981
|
+
detach(panelId) {
|
|
982
|
+
const panel = this.panels.get(panelId);
|
|
983
|
+
if (!panel) return false;
|
|
984
|
+
const previousGroup = getConnectedGroup(panel, this.panels);
|
|
985
|
+
detachFromGroup(panel, this.panels);
|
|
986
|
+
this.emit("panel:detached", { panel, previousGroup });
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
/**
|
|
990
|
+
* Update snapped positions (call after collapse/expand or resize)
|
|
991
|
+
*/
|
|
992
|
+
updatePositions() {
|
|
993
|
+
updateSnappedPositions(this.panels, this.config);
|
|
994
|
+
}
|
|
995
|
+
// ==================== Anchor Management ====================
|
|
996
|
+
/**
|
|
997
|
+
* Add a custom anchor
|
|
998
|
+
*/
|
|
999
|
+
addAnchor(config) {
|
|
1000
|
+
return this.anchorManager.addAnchor(config);
|
|
1001
|
+
}
|
|
1002
|
+
/**
|
|
1003
|
+
* Add a preset anchor
|
|
1004
|
+
*/
|
|
1005
|
+
addPresetAnchor(preset) {
|
|
1006
|
+
return this.anchorManager.addPresetAnchor(preset);
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* Remove an anchor
|
|
1010
|
+
*/
|
|
1011
|
+
removeAnchor(id) {
|
|
1012
|
+
return this.anchorManager.removeAnchor(id);
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Get all anchors
|
|
1016
|
+
*/
|
|
1017
|
+
getAnchors() {
|
|
1018
|
+
return this.anchorManager.getAnchors();
|
|
1019
|
+
}
|
|
1020
|
+
// ==================== Drag Callbacks ====================
|
|
1021
|
+
handleDragStart(state) {
|
|
1022
|
+
this.anchorManager.showIndicators(null);
|
|
1023
|
+
this.emit("drag:start", {
|
|
1024
|
+
panel: state.grabbedPanel,
|
|
1025
|
+
mode: state.mode,
|
|
1026
|
+
movingPanels: state.movingPanels
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
handleDragMove(state, position, snapTarget, anchorResult) {
|
|
1030
|
+
this.snapPreview.update(snapTarget);
|
|
1031
|
+
this.anchorManager.showIndicators(anchorResult?.anchor ?? null);
|
|
1032
|
+
this.emit("drag:move", {
|
|
1033
|
+
panel: state.grabbedPanel,
|
|
1034
|
+
position,
|
|
1035
|
+
snapTarget,
|
|
1036
|
+
anchorTarget: anchorResult
|
|
1037
|
+
});
|
|
1038
|
+
}
|
|
1039
|
+
handleDragEnd(state, snapTarget, anchorResult) {
|
|
1040
|
+
this.snapPreview.hide();
|
|
1041
|
+
this.anchorManager.hideIndicators();
|
|
1042
|
+
const rect = state.grabbedPanel.element.getBoundingClientRect();
|
|
1043
|
+
if (snapTarget) {
|
|
1044
|
+
const targetPanel = this.panels.get(snapTarget.targetId);
|
|
1045
|
+
if (targetPanel) {
|
|
1046
|
+
this.emit("snap:panel", {
|
|
1047
|
+
movingPanels: state.movingPanels,
|
|
1048
|
+
targetPanel,
|
|
1049
|
+
side: snapTarget.side
|
|
1050
|
+
});
|
|
1051
|
+
}
|
|
1052
|
+
} else if (anchorResult) {
|
|
1053
|
+
this.emit("snap:anchor", {
|
|
1054
|
+
movingPanels: state.movingPanels,
|
|
1055
|
+
anchor: anchorResult.anchor
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
this.emit("drag:end", {
|
|
1059
|
+
panel: state.grabbedPanel,
|
|
1060
|
+
finalPosition: { x: rect.left, y: rect.top },
|
|
1061
|
+
snappedToPanel: snapTarget !== null,
|
|
1062
|
+
snappedToAnchor: anchorResult !== null
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
// ==================== Event System ====================
|
|
1066
|
+
/**
|
|
1067
|
+
* Subscribe to an event
|
|
1068
|
+
*/
|
|
1069
|
+
on(event, listener) {
|
|
1070
|
+
if (!this.eventListeners.has(event)) {
|
|
1071
|
+
this.eventListeners.set(event, /* @__PURE__ */ new Set());
|
|
1072
|
+
}
|
|
1073
|
+
this.eventListeners.get(event).add(listener);
|
|
1074
|
+
return () => this.off(event, listener);
|
|
1075
|
+
}
|
|
1076
|
+
/**
|
|
1077
|
+
* Unsubscribe from an event
|
|
1078
|
+
*/
|
|
1079
|
+
off(event, listener) {
|
|
1080
|
+
this.eventListeners.get(event)?.delete(listener);
|
|
1081
|
+
}
|
|
1082
|
+
/**
|
|
1083
|
+
* Emit an event
|
|
1084
|
+
*/
|
|
1085
|
+
emit(event, data) {
|
|
1086
|
+
const listeners = this.eventListeners.get(event);
|
|
1087
|
+
if (listeners) {
|
|
1088
|
+
for (const listener of listeners) {
|
|
1089
|
+
listener(data);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
// ==================== Positioning ====================
|
|
1094
|
+
/**
|
|
1095
|
+
* Position panels in a row from right edge
|
|
1096
|
+
*/
|
|
1097
|
+
positionPanelsFromRight(panelIds, gap = 0) {
|
|
1098
|
+
let rightEdge = window.innerWidth - this.config.panelMargin;
|
|
1099
|
+
for (const id of panelIds) {
|
|
1100
|
+
const state = this.panels.get(id);
|
|
1101
|
+
if (!state) continue;
|
|
1102
|
+
const width = state.element.offsetWidth;
|
|
1103
|
+
setPanelPosition(state, rightEdge - width, this.config.panelMargin);
|
|
1104
|
+
rightEdge -= width + gap;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
/**
|
|
1108
|
+
* Position panels in a row from left edge
|
|
1109
|
+
*/
|
|
1110
|
+
positionPanelsFromLeft(panelIds, gap = 0) {
|
|
1111
|
+
let leftEdge = this.config.panelMargin;
|
|
1112
|
+
for (const id of panelIds) {
|
|
1113
|
+
const state = this.panels.get(id);
|
|
1114
|
+
if (!state) continue;
|
|
1115
|
+
setPanelPosition(state, leftEdge, this.config.panelMargin);
|
|
1116
|
+
leftEdge += state.element.offsetWidth + gap;
|
|
1117
|
+
}
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* Set up initial snap chain for a list of panels (left to right)
|
|
1121
|
+
*/
|
|
1122
|
+
createSnapChain(panelIds) {
|
|
1123
|
+
for (let i = 0; i < panelIds.length - 1; i++) {
|
|
1124
|
+
const leftPanel = this.panels.get(panelIds[i]);
|
|
1125
|
+
const rightPanel = this.panels.get(panelIds[i + 1]);
|
|
1126
|
+
if (leftPanel && rightPanel) {
|
|
1127
|
+
snapPanels(leftPanel, rightPanel);
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
// ==================== Configuration ====================
|
|
1132
|
+
/**
|
|
1133
|
+
* Get the current configuration
|
|
1134
|
+
*/
|
|
1135
|
+
getConfig() {
|
|
1136
|
+
return { ...this.config };
|
|
1137
|
+
}
|
|
1138
|
+
/**
|
|
1139
|
+
* Get the CSS classes used
|
|
1140
|
+
*/
|
|
1141
|
+
getClasses() {
|
|
1142
|
+
return { ...this.classes };
|
|
1143
|
+
}
|
|
1144
|
+
/**
|
|
1145
|
+
* Check if dragging is currently active
|
|
1146
|
+
*/
|
|
1147
|
+
isDragging() {
|
|
1148
|
+
return this.dragManager.isActive();
|
|
1149
|
+
}
|
|
1150
|
+
// ==================== Cleanup ====================
|
|
1151
|
+
/**
|
|
1152
|
+
* Destroy the TabManager and clean up resources
|
|
1153
|
+
*/
|
|
1154
|
+
destroy() {
|
|
1155
|
+
this.dragManager.destroy();
|
|
1156
|
+
this.anchorManager.destroy();
|
|
1157
|
+
this.snapPreview.destroy();
|
|
1158
|
+
for (const panel of this.panels.values()) {
|
|
1159
|
+
if (!panel.config.element) {
|
|
1160
|
+
panel.element.remove();
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
this.panels.clear();
|
|
1164
|
+
this.eventListeners.clear();
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1168
|
+
0 && (module.exports = {
|
|
1169
|
+
AnchorManager,
|
|
1170
|
+
DragManager,
|
|
1171
|
+
SnapPreview,
|
|
1172
|
+
TabManager,
|
|
1173
|
+
areInSameChain,
|
|
1174
|
+
createPanelElement,
|
|
1175
|
+
createPanelState,
|
|
1176
|
+
createPresetAnchor,
|
|
1177
|
+
detachFromGroup,
|
|
1178
|
+
findSnapTarget,
|
|
1179
|
+
getConnectedGroup,
|
|
1180
|
+
getDefaultAnchorConfigs,
|
|
1181
|
+
getDefaultZIndex,
|
|
1182
|
+
getDragZIndex,
|
|
1183
|
+
getLeftmostPanel,
|
|
1184
|
+
getPanelDimensions,
|
|
1185
|
+
getPanelPosition,
|
|
1186
|
+
getRightmostPanel,
|
|
1187
|
+
setPanelPosition,
|
|
1188
|
+
setPanelZIndex,
|
|
1189
|
+
snapPanels,
|
|
1190
|
+
snapPanelsToTarget,
|
|
1191
|
+
toggleCollapse,
|
|
1192
|
+
unsnap,
|
|
1193
|
+
updateSnappedPositions
|
|
1194
|
+
});
|