split-dock 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -0
- package/package.json +39 -0
- package/src/dock.js +156 -0
- package/src/frame.js +189 -0
- package/src/handles/dock-drop-handler.js +187 -0
- package/src/handles/drop-handler.js +348 -0
- package/src/handles/frame-adjust-handler.js +119 -0
- package/src/handles/tab-bar-drop-handler.js +173 -0
- package/src/index.js +115 -0
- package/src/panel.js +122 -0
- package/src/style.css +276 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { Frame } from '../frame.js';
|
|
2
|
+
import { Dock } from '../dock.js';
|
|
3
|
+
|
|
4
|
+
export class DropHandler {
|
|
5
|
+
constructor(dock) {
|
|
6
|
+
this.dock = dock;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
onDragOver(e) {
|
|
10
|
+
if (!this.dock.splitDock?.draggedPanel) return;
|
|
11
|
+
|
|
12
|
+
e.preventDefault();
|
|
13
|
+
e.stopPropagation();
|
|
14
|
+
e.dataTransfer.dropEffect = 'move';
|
|
15
|
+
|
|
16
|
+
// Clear previous dock's indicators if switching to a new dock
|
|
17
|
+
if (this.dock.splitDock.currentHoverDock && this.dock.splitDock.currentHoverDock !== this.dock) {
|
|
18
|
+
this.clearAllIndicators(this.dock.splitDock.currentHoverDock);
|
|
19
|
+
}
|
|
20
|
+
this.dock.splitDock.currentHoverDock = this.dock;
|
|
21
|
+
|
|
22
|
+
// Clear all indicators first
|
|
23
|
+
this.clearAllIndicators(this.dock);
|
|
24
|
+
|
|
25
|
+
const zone = this.getDropZone(e);
|
|
26
|
+
if (!zone) return;
|
|
27
|
+
|
|
28
|
+
// Apply the appropriate indicator based on zone
|
|
29
|
+
if (zone.type === 'navbar') {
|
|
30
|
+
this.showNavbarIndicator(zone, e);
|
|
31
|
+
} else {
|
|
32
|
+
this.dock.element.classList.add(`drop-${zone.type}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
onDragLeave(e) {
|
|
37
|
+
const rect = this.dock.element.getBoundingClientRect();
|
|
38
|
+
if (e.clientX < rect.left || e.clientX >= rect.right ||
|
|
39
|
+
e.clientY < rect.top || e.clientY >= rect.bottom) {
|
|
40
|
+
this.clearAllIndicators(this.dock);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
onDrop(e) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
e.stopPropagation();
|
|
47
|
+
|
|
48
|
+
if (!this.dock.splitDock) return;
|
|
49
|
+
|
|
50
|
+
const panel = this.dock.splitDock.draggedPanel;
|
|
51
|
+
const fromDock = this.dock.splitDock.draggedFromDock;
|
|
52
|
+
if (!panel || !fromDock) return;
|
|
53
|
+
|
|
54
|
+
this.clearAllIndicators(this.dock);
|
|
55
|
+
|
|
56
|
+
const zone = this.getDropZone(e);
|
|
57
|
+
if (!zone) return;
|
|
58
|
+
|
|
59
|
+
if (zone.type === 'navbar') {
|
|
60
|
+
this.handleNavbarDrop(zone, panel, fromDock);
|
|
61
|
+
} else if (zone.type === 'center') {
|
|
62
|
+
if (fromDock !== this.dock) {
|
|
63
|
+
this.dock.acceptPanel(panel, fromDock);
|
|
64
|
+
}
|
|
65
|
+
} else {
|
|
66
|
+
// Don't allow splitting if dragging the only panel from the same dock
|
|
67
|
+
if (fromDock === this.dock && fromDock.panels.length === 1) return;
|
|
68
|
+
this.createSplit(zone.type, panel, fromDock);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
getDropZone(e) {
|
|
73
|
+
// First check if over THIS dock's navbar
|
|
74
|
+
const navbarRect = this.dock.navbar.getBoundingClientRect();
|
|
75
|
+
if (e.clientX >= navbarRect.left && e.clientX <= navbarRect.right &&
|
|
76
|
+
e.clientY >= navbarRect.top && e.clientY <= navbarRect.bottom) {
|
|
77
|
+
return { type: 'navbar', element: this.dock.navbar };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Check dock split zones
|
|
81
|
+
const rect = this.dock.element.getBoundingClientRect();
|
|
82
|
+
const x = e.clientX - rect.left;
|
|
83
|
+
const y = e.clientY - rect.top;
|
|
84
|
+
const edgeSize = Math.min(rect.width, rect.height) * 0.33; // 33% of the smaller dimension
|
|
85
|
+
|
|
86
|
+
if (y < edgeSize) return { type: 'top' };
|
|
87
|
+
if (y > rect.height - edgeSize) return { type: 'bottom' };
|
|
88
|
+
if (x < edgeSize) return { type: 'left' };
|
|
89
|
+
if (x > rect.width - edgeSize) return { type: 'right' };
|
|
90
|
+
|
|
91
|
+
return { type: 'center' };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
clearAllIndicators(dock) {
|
|
95
|
+
// Clear dock zone indicators
|
|
96
|
+
dock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
|
|
97
|
+
|
|
98
|
+
// Clear tab indicators
|
|
99
|
+
const titleElements = Array.from(dock.navbar.querySelectorAll('.sd-panel-title'));
|
|
100
|
+
titleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
showNavbarIndicator(zone, e) {
|
|
104
|
+
const titleElements = Array.from(this.dock.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
|
|
105
|
+
|
|
106
|
+
if (titleElements.length === 0) return;
|
|
107
|
+
|
|
108
|
+
const firstTab = titleElements[0];
|
|
109
|
+
const lastTab = titleElements[titleElements.length - 1];
|
|
110
|
+
const firstRect = firstTab.getBoundingClientRect();
|
|
111
|
+
const lastRect = lastTab.getBoundingClientRect();
|
|
112
|
+
|
|
113
|
+
if (e.clientX < firstRect.left) {
|
|
114
|
+
firstTab.classList.add('drop-before');
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (e.clientX > lastRect.right) {
|
|
119
|
+
lastTab.classList.add('drop-after');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const hoveredTitle = titleElements.find(el => {
|
|
124
|
+
const rect = el.getBoundingClientRect();
|
|
125
|
+
return e.clientX >= rect.left && e.clientX <= rect.right;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
if (hoveredTitle) {
|
|
129
|
+
const rect = hoveredTitle.getBoundingClientRect();
|
|
130
|
+
const midpoint = rect.left + rect.width / 2;
|
|
131
|
+
|
|
132
|
+
if (e.clientX < midpoint) {
|
|
133
|
+
hoveredTitle.classList.add('drop-before');
|
|
134
|
+
} else {
|
|
135
|
+
hoveredTitle.classList.add('drop-after');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
handleNavbarDrop(zone, panel, fromDock) {
|
|
141
|
+
const targetInfo = this.getTargetPanelInfo(event);
|
|
142
|
+
|
|
143
|
+
if (!targetInfo) {
|
|
144
|
+
// No specific target, add to end if from different dock
|
|
145
|
+
if (fromDock !== this.dock) {
|
|
146
|
+
this.dock.acceptPanel(panel, fromDock);
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (fromDock === this.dock) {
|
|
152
|
+
// Reordering within same dock
|
|
153
|
+
this.reorderPanel(panel, targetInfo.panel, targetInfo.position);
|
|
154
|
+
} else {
|
|
155
|
+
// Moving from different dock
|
|
156
|
+
this.movePanel(panel, fromDock, targetInfo.panel, targetInfo.position);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
getTargetPanelInfo(e) {
|
|
161
|
+
const titleElements = Array.from(this.dock.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
|
|
162
|
+
if (titleElements.length === 0) return null;
|
|
163
|
+
|
|
164
|
+
const firstTab = titleElements[0];
|
|
165
|
+
const lastTab = titleElements[titleElements.length - 1];
|
|
166
|
+
const firstRect = firstTab.getBoundingClientRect();
|
|
167
|
+
const lastRect = lastTab.getBoundingClientRect();
|
|
168
|
+
|
|
169
|
+
// Before first tab
|
|
170
|
+
if (e.clientX < firstRect.left) {
|
|
171
|
+
const targetPanel = this.dock.panels.find(p => p.titleElement === firstTab);
|
|
172
|
+
return targetPanel ? { panel: targetPanel, position: 'before' } : null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// After last tab
|
|
176
|
+
if (e.clientX > lastRect.right) {
|
|
177
|
+
const targetPanel = this.dock.panels.find(p => p.titleElement === lastTab);
|
|
178
|
+
return targetPanel ? { panel: targetPanel, position: 'after' } : null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Over a specific tab
|
|
182
|
+
const hoveredTitle = titleElements.find(el => {
|
|
183
|
+
const rect = el.getBoundingClientRect();
|
|
184
|
+
return e.clientX >= rect.left && e.clientX <= rect.right;
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (hoveredTitle) {
|
|
188
|
+
const rect = hoveredTitle.getBoundingClientRect();
|
|
189
|
+
const midpoint = rect.left + rect.width / 2;
|
|
190
|
+
const targetPanel = this.dock.panels.find(p => p.titleElement === hoveredTitle);
|
|
191
|
+
|
|
192
|
+
if (targetPanel) {
|
|
193
|
+
return {
|
|
194
|
+
panel: targetPanel,
|
|
195
|
+
position: e.clientX < midpoint ? 'before' : 'after'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
reorderPanel(panel, targetPanel, position) {
|
|
204
|
+
const panelIndex = this.dock.panels.indexOf(panel);
|
|
205
|
+
const targetIndex = this.dock.panels.indexOf(targetPanel);
|
|
206
|
+
|
|
207
|
+
if (panelIndex === -1 || targetIndex === -1) return;
|
|
208
|
+
|
|
209
|
+
// Remove panel from current position
|
|
210
|
+
this.dock.panels.splice(panelIndex, 1);
|
|
211
|
+
|
|
212
|
+
// Calculate new index
|
|
213
|
+
let newIndex = targetIndex;
|
|
214
|
+
if (position === 'after') {
|
|
215
|
+
newIndex++;
|
|
216
|
+
}
|
|
217
|
+
// Adjust if we removed panel before target
|
|
218
|
+
if (panelIndex < targetIndex) {
|
|
219
|
+
newIndex--;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Insert at new position
|
|
223
|
+
this.dock.panels.splice(newIndex, 0, panel);
|
|
224
|
+
|
|
225
|
+
// Update DOM
|
|
226
|
+
if (position === 'before') {
|
|
227
|
+
this.dock.navbar.insertBefore(panel.titleElement, targetPanel.titleElement);
|
|
228
|
+
} else {
|
|
229
|
+
this.dock.navbar.insertBefore(panel.titleElement, targetPanel.titleElement.nextSibling);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
movePanel(panel, fromDock, targetPanel, position) {
|
|
234
|
+
// Remove from source dock
|
|
235
|
+
fromDock.removePanel(panel, true);
|
|
236
|
+
|
|
237
|
+
// Add to target dock at specific position
|
|
238
|
+
const targetIndex = this.dock.panels.indexOf(targetPanel);
|
|
239
|
+
if (targetIndex === -1) {
|
|
240
|
+
this.dock.addPanel(panel);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const insertIndex = position === 'before' ? targetIndex : targetIndex + 1;
|
|
245
|
+
|
|
246
|
+
panel.dock = this.dock;
|
|
247
|
+
this.dock.panels.splice(insertIndex, 0, panel);
|
|
248
|
+
|
|
249
|
+
// Update DOM
|
|
250
|
+
if (position === 'before') {
|
|
251
|
+
this.dock.navbar.insertBefore(panel.titleElement, targetPanel.titleElement);
|
|
252
|
+
} else {
|
|
253
|
+
this.dock.navbar.insertBefore(panel.titleElement, targetPanel.titleElement.nextSibling);
|
|
254
|
+
}
|
|
255
|
+
this.dock.content.appendChild(panel.contentElement);
|
|
256
|
+
|
|
257
|
+
this.dock.setActivePanel(panel);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
createSplit(direction, panel, fromDock) {
|
|
261
|
+
if (!this.dock.parentFrame) return;
|
|
262
|
+
|
|
263
|
+
const currentIndex = this.dock.parentFrame.children.indexOf(this.dock);
|
|
264
|
+
if (currentIndex === -1) return;
|
|
265
|
+
|
|
266
|
+
const newDock = new Dock(null, this.dock.parentFrame);
|
|
267
|
+
newDock.addPanel(panel);
|
|
268
|
+
fromDock.removePanel(panel, true);
|
|
269
|
+
|
|
270
|
+
this.applySplit(direction, newDock, currentIndex);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
applySplit(direction, newDock, currentIndex) {
|
|
274
|
+
const parentFrame = this.dock.parentFrame;
|
|
275
|
+
const needsVerticalSplit = direction === 'top' || direction === 'bottom';
|
|
276
|
+
const needsHorizontalSplit = direction === 'left' || direction === 'right';
|
|
277
|
+
|
|
278
|
+
if ((needsVerticalSplit && parentFrame.splitDirection === 'vertical') ||
|
|
279
|
+
(needsHorizontalSplit && parentFrame.splitDirection === 'horizontal')) {
|
|
280
|
+
this.splitIntoParent(direction, newDock, currentIndex);
|
|
281
|
+
} else if (parentFrame.splitDirection === null && parentFrame.children.length === 1) {
|
|
282
|
+
this.insertIntoParent(direction, newDock, currentIndex);
|
|
283
|
+
} else {
|
|
284
|
+
this.createNestedSplit(direction, newDock);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
splitIntoParent(direction, newDock, currentIndex) {
|
|
289
|
+
const insertBefore = direction === 'top' || direction === 'left';
|
|
290
|
+
const insertIndex = insertBefore ? currentIndex : currentIndex + 1;
|
|
291
|
+
|
|
292
|
+
this.dock.parentFrame.children.splice(insertIndex, 0, newDock);
|
|
293
|
+
|
|
294
|
+
if (insertBefore) {
|
|
295
|
+
this.dock.parentFrame.element.insertBefore(newDock.element, this.dock.element);
|
|
296
|
+
} else {
|
|
297
|
+
this.dock.parentFrame.element.insertBefore(newDock.element, this.dock.element.nextSibling);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.dock.element.style.flex = '1 1 0px';
|
|
301
|
+
newDock.element.style.flex = '1 1 0px';
|
|
302
|
+
|
|
303
|
+
this.dock.parentFrame.updateStyles();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
insertIntoParent(direction, newDock, currentIndex) {
|
|
307
|
+
const needsVerticalSplit = direction === 'top' || direction === 'bottom';
|
|
308
|
+
// Default to vertical split direction
|
|
309
|
+
this.dock.parentFrame.splitDirection = needsVerticalSplit ? 'vertical' : 'horizontal';
|
|
310
|
+
|
|
311
|
+
this.splitIntoParent(direction, newDock, currentIndex);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
createNestedSplit(direction, newDock) {
|
|
315
|
+
const parentFrame = this.dock.parentFrame;
|
|
316
|
+
const currentIndex = parentFrame.children.indexOf(this.dock);
|
|
317
|
+
|
|
318
|
+
const needsVerticalSplit = direction === 'top' || direction === 'bottom';
|
|
319
|
+
const newSplitDirection = needsVerticalSplit ? 'vertical' : 'horizontal';
|
|
320
|
+
|
|
321
|
+
const wrapperFrame = new Frame(null, this.dock.splitDock);
|
|
322
|
+
wrapperFrame.splitDirection = newSplitDirection;
|
|
323
|
+
wrapperFrame.parentFrame = parentFrame;
|
|
324
|
+
|
|
325
|
+
parentFrame.children[currentIndex] = wrapperFrame;
|
|
326
|
+
parentFrame.element.insertBefore(wrapperFrame.element, this.dock.element);
|
|
327
|
+
|
|
328
|
+
this.dock.element.remove();
|
|
329
|
+
this.dock.parentFrame = wrapperFrame;
|
|
330
|
+
newDock.parentFrame = wrapperFrame;
|
|
331
|
+
|
|
332
|
+
const insertBefore = direction === 'top' || direction === 'left';
|
|
333
|
+
if (insertBefore) {
|
|
334
|
+
wrapperFrame.children = [newDock, this.dock];
|
|
335
|
+
wrapperFrame.element.appendChild(newDock.element);
|
|
336
|
+
wrapperFrame.element.appendChild(this.dock.element);
|
|
337
|
+
} else {
|
|
338
|
+
wrapperFrame.children = [this.dock, newDock];
|
|
339
|
+
wrapperFrame.element.appendChild(this.dock.element);
|
|
340
|
+
wrapperFrame.element.appendChild(newDock.element);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
this.dock.element.style.flex = '1 1 0px';
|
|
344
|
+
newDock.element.style.flex = '1 1 0px';
|
|
345
|
+
|
|
346
|
+
wrapperFrame.updateStyles();
|
|
347
|
+
}
|
|
348
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
export class FrameAdjustHandler {
|
|
2
|
+
constructor(frame) {
|
|
3
|
+
this.frame = frame;
|
|
4
|
+
this.handles = [];
|
|
5
|
+
this.handleListeners = [];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
destroy() {
|
|
9
|
+
// Remove all resize handles from DOM
|
|
10
|
+
this.handles.forEach(handle => {
|
|
11
|
+
if (handle && handle.parentNode) {
|
|
12
|
+
handle.remove();
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
// Clear arrays
|
|
17
|
+
this.handles = [];
|
|
18
|
+
this.handleListeners = [];
|
|
19
|
+
|
|
20
|
+
// Clear frame reference
|
|
21
|
+
this.frame = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setupResizeHandles() {
|
|
25
|
+
// Clear existing handles
|
|
26
|
+
this.handles.forEach(handle => handle.remove());
|
|
27
|
+
this.handles = [];
|
|
28
|
+
|
|
29
|
+
// Only add resize handles if we have a split direction and multiple children
|
|
30
|
+
if (!this.frame.splitDirection || this.frame.children.length < 2) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Insert resize handles between children
|
|
35
|
+
for (let i = 0; i < this.frame.children.length - 1; i++) {
|
|
36
|
+
const handle = document.createElement('div');
|
|
37
|
+
handle.className = `sd-resize-handle ${this.frame.splitDirection}`;
|
|
38
|
+
|
|
39
|
+
// Store indices for resize
|
|
40
|
+
handle.dataset.leftIndex = i;
|
|
41
|
+
handle.dataset.rightIndex = i + 1;
|
|
42
|
+
|
|
43
|
+
// Insert handle after the current child
|
|
44
|
+
const currentChild = this.frame.children[i].element;
|
|
45
|
+
currentChild.parentNode.insertBefore(handle, currentChild.nextSibling);
|
|
46
|
+
|
|
47
|
+
this.handles.push(handle);
|
|
48
|
+
this.setupResizeListener(handle, i, i + 1);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
setupResizeListener(handle, leftIndex, rightIndex) {
|
|
53
|
+
let startPos = 0;
|
|
54
|
+
let leftStartSize = 0;
|
|
55
|
+
let rightStartSize = 0;
|
|
56
|
+
let leftChild = null;
|
|
57
|
+
let rightChild = null;
|
|
58
|
+
let containerSize = 0;
|
|
59
|
+
|
|
60
|
+
const minSize = 100; // Minimum size in pixels
|
|
61
|
+
|
|
62
|
+
const onMouseDown = (e) => {
|
|
63
|
+
e.preventDefault();
|
|
64
|
+
|
|
65
|
+
leftChild = this.frame.children[leftIndex];
|
|
66
|
+
rightChild = this.frame.children[rightIndex];
|
|
67
|
+
|
|
68
|
+
if (!leftChild || !rightChild) return;
|
|
69
|
+
|
|
70
|
+
startPos = this.frame.splitDirection === 'horizontal' ? e.clientX : e.clientY;
|
|
71
|
+
|
|
72
|
+
const leftRect = leftChild.element.getBoundingClientRect();
|
|
73
|
+
const rightRect = rightChild.element.getBoundingClientRect();
|
|
74
|
+
const containerRect = this.frame.element.getBoundingClientRect();
|
|
75
|
+
|
|
76
|
+
leftStartSize = this.frame.splitDirection === 'horizontal' ? leftRect.width : leftRect.height;
|
|
77
|
+
rightStartSize = this.frame.splitDirection === 'horizontal' ? rightRect.width : rightRect.height;
|
|
78
|
+
containerSize = this.frame.splitDirection === 'horizontal' ? containerRect.width : containerRect.height;
|
|
79
|
+
|
|
80
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
81
|
+
document.addEventListener('mouseup', onMouseUp);
|
|
82
|
+
|
|
83
|
+
// Add visual feedback
|
|
84
|
+
handle.style.background = '#1e88e5';
|
|
85
|
+
document.body.style.cursor = this.frame.splitDirection === 'horizontal' ? 'col-resize' : 'row-resize';
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const onMouseMove = (e) => {
|
|
89
|
+
if (!leftChild || !rightChild) return;
|
|
90
|
+
|
|
91
|
+
const currentPos = this.frame.splitDirection === 'horizontal' ? e.clientX : e.clientY;
|
|
92
|
+
const delta = currentPos - startPos;
|
|
93
|
+
|
|
94
|
+
const newLeftSize = leftStartSize + delta;
|
|
95
|
+
const newRightSize = rightStartSize - delta;
|
|
96
|
+
|
|
97
|
+
// Enforce minimum sizes
|
|
98
|
+
if (newLeftSize >= minSize && newRightSize >= minSize) {
|
|
99
|
+
// Calculate flex values relative to the entire container
|
|
100
|
+
const leftFlex = newLeftSize / containerSize;
|
|
101
|
+
const rightFlex = newRightSize / containerSize;
|
|
102
|
+
|
|
103
|
+
leftChild.element.style.flex = `${leftFlex} 1 0px`;
|
|
104
|
+
rightChild.element.style.flex = `${rightFlex} 1 0px`;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const onMouseUp = () => {
|
|
109
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
110
|
+
document.removeEventListener('mouseup', onMouseUp);
|
|
111
|
+
|
|
112
|
+
// Remove visual feedback
|
|
113
|
+
handle.style.background = '';
|
|
114
|
+
document.body.style.cursor = '';
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
handle.addEventListener('mousedown', onMouseDown);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export class TabBarDropHandler {
|
|
2
|
+
constructor(dock) {
|
|
3
|
+
this.dock = dock;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
onDragOver(e) {
|
|
7
|
+
if (!this.dock.splitDock?.draggedPanel) return;
|
|
8
|
+
|
|
9
|
+
e.preventDefault();
|
|
10
|
+
e.stopPropagation();
|
|
11
|
+
e.dataTransfer.dropEffect = 'move';
|
|
12
|
+
|
|
13
|
+
// Clear tab indicators from previous dock if switching
|
|
14
|
+
if (this.dock.splitDock.currentHoverDock && this.dock.splitDock.currentHoverDock !== this.dock) {
|
|
15
|
+
const prevTitleElements = Array.from(this.dock.splitDock.currentHoverDock.navbar.querySelectorAll('.sd-panel-title'));
|
|
16
|
+
prevTitleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
17
|
+
this.dock.splitDock.currentHoverDock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
|
|
18
|
+
}
|
|
19
|
+
this.dock.splitDock.currentHoverDock = this.dock;
|
|
20
|
+
|
|
21
|
+
// Clear any dock zone indicators since we're over the navbar
|
|
22
|
+
this.dock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
|
|
23
|
+
|
|
24
|
+
const titleElements = Array.from(this.dock.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
|
|
25
|
+
|
|
26
|
+
// Remove all drop markers
|
|
27
|
+
titleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
28
|
+
|
|
29
|
+
if (titleElements.length === 0) return;
|
|
30
|
+
|
|
31
|
+
const firstTab = titleElements[0];
|
|
32
|
+
const lastTab = titleElements[titleElements.length - 1];
|
|
33
|
+
const firstRect = firstTab.getBoundingClientRect();
|
|
34
|
+
const lastRect = lastTab.getBoundingClientRect();
|
|
35
|
+
|
|
36
|
+
if (e.clientX < firstRect.left) {
|
|
37
|
+
firstTab.classList.add('drop-before');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (e.clientX > lastRect.right) {
|
|
42
|
+
lastTab.classList.add('drop-after');
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const hoveredTitle = titleElements.find(el => {
|
|
47
|
+
const rect = el.getBoundingClientRect();
|
|
48
|
+
return e.clientX >= rect.left && e.clientX <= rect.right;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (hoveredTitle) {
|
|
52
|
+
const rect = hoveredTitle.getBoundingClientRect();
|
|
53
|
+
const midpoint = rect.left + rect.width / 2;
|
|
54
|
+
|
|
55
|
+
if (e.clientX < midpoint) {
|
|
56
|
+
hoveredTitle.classList.add('drop-before');
|
|
57
|
+
} else {
|
|
58
|
+
hoveredTitle.classList.add('drop-after');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
onDragLeave(e) {
|
|
64
|
+
const navbarRect = this.dock.navbar.getBoundingClientRect();
|
|
65
|
+
if (e.clientX < navbarRect.left || e.clientX >= navbarRect.right ||
|
|
66
|
+
e.clientY < navbarRect.top || e.clientY >= navbarRect.bottom) {
|
|
67
|
+
const titleElements = Array.from(this.dock.navbar.querySelectorAll('.sd-panel-title'));
|
|
68
|
+
titleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
onDrop(e) {
|
|
73
|
+
if (!this.dock.splitDock?.draggedPanel) return;
|
|
74
|
+
|
|
75
|
+
e.preventDefault();
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
|
|
78
|
+
const panel = this.dock.splitDock.draggedPanel;
|
|
79
|
+
const fromDock = this.dock.splitDock.draggedFromDock;
|
|
80
|
+
const titleElements = Array.from(this.dock.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
|
|
81
|
+
|
|
82
|
+
titleElements.forEach(el => el.classList.remove('drop-before', 'drop-after'));
|
|
83
|
+
|
|
84
|
+
if (titleElements.length === 0) {
|
|
85
|
+
if (fromDock !== this.dock) {
|
|
86
|
+
this.dock.acceptPanel(panel, fromDock);
|
|
87
|
+
}
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const { targetPanel, insertBefore } = this.getTargetPanelInfo(e, titleElements);
|
|
92
|
+
|
|
93
|
+
if (!targetPanel) {
|
|
94
|
+
if (fromDock !== this.dock) {
|
|
95
|
+
this.dock.acceptPanel(panel, fromDock);
|
|
96
|
+
}
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (fromDock === this.dock) {
|
|
101
|
+
this.reorderPanel(panel, targetPanel, insertBefore);
|
|
102
|
+
} else {
|
|
103
|
+
this.movePanel(panel, fromDock, targetPanel, insertBefore);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
getTargetPanelInfo(e, titleElements) {
|
|
108
|
+
const firstTab = titleElements[0];
|
|
109
|
+
const lastTab = titleElements[titleElements.length - 1];
|
|
110
|
+
const firstRect = firstTab.getBoundingClientRect();
|
|
111
|
+
const lastRect = lastTab.getBoundingClientRect();
|
|
112
|
+
|
|
113
|
+
if (e.clientX < firstRect.left) {
|
|
114
|
+
return {
|
|
115
|
+
targetPanel: this.dock.panels.find(p => p.titleElement === firstTab),
|
|
116
|
+
insertBefore: true
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (e.clientX > lastRect.right) {
|
|
121
|
+
return {
|
|
122
|
+
targetPanel: this.dock.panels.find(p => p.titleElement === lastTab),
|
|
123
|
+
insertBefore: false
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const hoveredTitle = titleElements.find(el => {
|
|
128
|
+
const rect = el.getBoundingClientRect();
|
|
129
|
+
return e.clientX >= rect.left && e.clientX <= rect.right;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (!hoveredTitle) {
|
|
133
|
+
return { targetPanel: null, insertBefore: true };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const targetPanel = this.dock.panels.find(p => p.titleElement === hoveredTitle);
|
|
137
|
+
const rect = hoveredTitle.getBoundingClientRect();
|
|
138
|
+
const midpoint = rect.left + rect.width / 2;
|
|
139
|
+
|
|
140
|
+
return { targetPanel, insertBefore: e.clientX < midpoint };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
reorderPanel(panel, targetPanel, insertBefore) {
|
|
144
|
+
const oldIndex = this.dock.panels.indexOf(panel);
|
|
145
|
+
const newIndex = this.dock.panels.indexOf(targetPanel);
|
|
146
|
+
|
|
147
|
+
if (oldIndex === newIndex) return;
|
|
148
|
+
|
|
149
|
+
this.dock.panels.splice(oldIndex, 1);
|
|
150
|
+
|
|
151
|
+
const adjustedIndex = oldIndex < newIndex ? newIndex : newIndex + (insertBefore ? 0 : 1);
|
|
152
|
+
this.dock.panels.splice(insertBefore ? newIndex : adjustedIndex, 0, panel);
|
|
153
|
+
|
|
154
|
+
const refElement = insertBefore ? targetPanel.titleElement : targetPanel.titleElement.nextSibling;
|
|
155
|
+
this.dock.navbar.insertBefore(panel.titleElement, refElement);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
movePanel(panel, fromDock, targetPanel, insertBefore) {
|
|
159
|
+
fromDock.removePanel(panel, true);
|
|
160
|
+
|
|
161
|
+
const targetIndex = this.dock.panels.indexOf(targetPanel);
|
|
162
|
+
const insertIndex = insertBefore ? targetIndex : targetIndex + 1;
|
|
163
|
+
|
|
164
|
+
panel.dock = this.dock;
|
|
165
|
+
this.dock.panels.splice(insertIndex, 0, panel);
|
|
166
|
+
this.dock.content.appendChild(panel.contentElement);
|
|
167
|
+
|
|
168
|
+
const refElement = insertBefore ? targetPanel.titleElement : targetPanel.titleElement.nextSibling;
|
|
169
|
+
this.dock.navbar.insertBefore(panel.titleElement, refElement);
|
|
170
|
+
|
|
171
|
+
this.dock.setActivePanel(panel);
|
|
172
|
+
}
|
|
173
|
+
}
|