@mintplayer/ng-bootstrap 20.6.0 → 20.6.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.
|
@@ -39,6 +39,7 @@ const templateHtml = `
|
|
|
39
39
|
box-sizing: border-box;
|
|
40
40
|
font-family: inherit;
|
|
41
41
|
color: inherit;
|
|
42
|
+
--dock-split-gap: 0.25rem;
|
|
42
43
|
}
|
|
43
44
|
|
|
44
45
|
.dock-root,
|
|
@@ -73,6 +74,13 @@ const templateHtml = `
|
|
|
73
74
|
z-index: 5;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
.dock-intersections-layer {
|
|
78
|
+
position: absolute;
|
|
79
|
+
inset: 0;
|
|
80
|
+
pointer-events: none;
|
|
81
|
+
z-index: 120;
|
|
82
|
+
}
|
|
83
|
+
|
|
76
84
|
.dock-floating {
|
|
77
85
|
position: absolute;
|
|
78
86
|
display: flex;
|
|
@@ -127,7 +135,8 @@ const templateHtml = `
|
|
|
127
135
|
transition: background 120ms ease;
|
|
128
136
|
}
|
|
129
137
|
|
|
130
|
-
.dock-floating__resizer:hover
|
|
138
|
+
.dock-floating__resizer:hover,
|
|
139
|
+
.dock-floating__resizer[data-resizing='true'] {
|
|
131
140
|
background: rgba(148, 163, 184, 0.4);
|
|
132
141
|
}
|
|
133
142
|
|
|
@@ -200,7 +209,7 @@ const templateHtml = `
|
|
|
200
209
|
.dock-split {
|
|
201
210
|
display: flex;
|
|
202
211
|
flex: 1 1 0;
|
|
203
|
-
gap:
|
|
212
|
+
gap: var(--dock-split-gap);
|
|
204
213
|
position: relative;
|
|
205
214
|
}
|
|
206
215
|
|
|
@@ -228,11 +237,17 @@ const templateHtml = `
|
|
|
228
237
|
.dock-split[data-direction="horizontal"] > .dock-split__divider {
|
|
229
238
|
width: 0.5rem;
|
|
230
239
|
cursor: col-resize;
|
|
240
|
+
/* Extend through perpendicular gaps for visual continuity */
|
|
241
|
+
margin-top: calc(var(--dock-split-gap) * -1);
|
|
242
|
+
margin-bottom: calc(var(--dock-split-gap) * -1);
|
|
231
243
|
}
|
|
232
244
|
|
|
233
245
|
.dock-split[data-direction="vertical"] > .dock-split__divider {
|
|
234
246
|
height: 0.5rem;
|
|
235
247
|
cursor: row-resize;
|
|
248
|
+
/* Extend through perpendicular gaps for visual continuity */
|
|
249
|
+
margin-left: calc(var(--dock-split-gap) * -1);
|
|
250
|
+
margin-right: calc(var(--dock-split-gap) * -1);
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
.dock-split__divider::after {
|
|
@@ -261,6 +276,45 @@ const templateHtml = `
|
|
|
261
276
|
background: rgba(59, 130, 246, 0.35);
|
|
262
277
|
}
|
|
263
278
|
|
|
279
|
+
.dock-intersection-handle {
|
|
280
|
+
position: absolute;
|
|
281
|
+
width: 1rem;
|
|
282
|
+
height: 1rem;
|
|
283
|
+
margin-left: -0.5rem;
|
|
284
|
+
margin-top: -0.5rem;
|
|
285
|
+
border-radius: 0.375rem;
|
|
286
|
+
background: rgba(59, 130, 246, 0.2);
|
|
287
|
+
border: 1px solid rgba(59, 130, 246, 0.6);
|
|
288
|
+
box-shadow: 0 2px 6px rgba(15, 23, 42, 0.2);
|
|
289
|
+
cursor: all-scroll;
|
|
290
|
+
pointer-events: auto;
|
|
291
|
+
opacity: 0;
|
|
292
|
+
transition: background 120ms ease, border-color 120ms ease, opacity 120ms ease;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.dock-intersection-handle:hover,
|
|
296
|
+
.dock-intersection-handle:focus-visible,
|
|
297
|
+
.dock-intersection-handle[data-visible='true'],
|
|
298
|
+
.dock-intersection-handle[data-resizing='true'] {
|
|
299
|
+
background: rgba(59, 130, 246, 0.35);
|
|
300
|
+
border-color: rgba(59, 130, 246, 0.9);
|
|
301
|
+
opacity: 1;
|
|
302
|
+
outline: none;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.dock-snap-marker {
|
|
306
|
+
position: absolute;
|
|
307
|
+
width: 6px;
|
|
308
|
+
height: 6px;
|
|
309
|
+
margin-left: -3px;
|
|
310
|
+
margin-top: -3px;
|
|
311
|
+
border-radius: 50%;
|
|
312
|
+
background: rgba(59, 130, 246, 0.7);
|
|
313
|
+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.15);
|
|
314
|
+
pointer-events: none;
|
|
315
|
+
z-index: 130;
|
|
316
|
+
}
|
|
317
|
+
|
|
264
318
|
.dock-stack {
|
|
265
319
|
display: flex;
|
|
266
320
|
flex-direction: column;
|
|
@@ -409,6 +463,7 @@ const templateHtml = `
|
|
|
409
463
|
<div class="dock-root">
|
|
410
464
|
<div class="dock-docked"></div>
|
|
411
465
|
<div class="dock-floating-layer"></div>
|
|
466
|
+
<div class="dock-intersections-layer dock-intersection-layer"></div>
|
|
412
467
|
</div>
|
|
413
468
|
<div class="dock-drop-indicator"></div>
|
|
414
469
|
<div class="dock-drop-joystick" data-visible="false">
|
|
@@ -477,8 +532,93 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
477
532
|
}
|
|
478
533
|
static get observedAttributes() {
|
|
479
534
|
return ['layout'];
|
|
535
|
+
// return ['layout', 'debug-snap-markers'];
|
|
480
536
|
}
|
|
481
537
|
static { this.instanceCounter = 0; }
|
|
538
|
+
renderSnapMarkersForDivider() {
|
|
539
|
+
if (!this.showSnapMarkers)
|
|
540
|
+
return;
|
|
541
|
+
const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
|
|
542
|
+
if (!layer)
|
|
543
|
+
return;
|
|
544
|
+
// Clear previous
|
|
545
|
+
Array.from(layer.querySelectorAll('.dock-snap-marker')).forEach((el) => el.remove());
|
|
546
|
+
if (!this.resizeState || !this.activeSnapAxis || this.activeSnapTargets.length === 0)
|
|
547
|
+
return;
|
|
548
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
549
|
+
const dRect = this.resizeState.divider.getBoundingClientRect();
|
|
550
|
+
if (this.activeSnapAxis === 'x') {
|
|
551
|
+
const y = dRect.top + dRect.height / 2 - rootRect.top;
|
|
552
|
+
this.activeSnapTargets.forEach((sx) => {
|
|
553
|
+
const dot = this.documentRef.createElement('div');
|
|
554
|
+
dot.className = 'dock-snap-marker';
|
|
555
|
+
dot.style.left = `${rootRect.left + sx - rootRect.left}px`;
|
|
556
|
+
dot.style.top = `${y}px`;
|
|
557
|
+
layer.appendChild(dot);
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
else if (this.activeSnapAxis === 'y') {
|
|
561
|
+
const x = dRect.left + dRect.width / 2 - rootRect.left;
|
|
562
|
+
this.activeSnapTargets.forEach((sy) => {
|
|
563
|
+
const dot = this.documentRef.createElement('div');
|
|
564
|
+
dot.className = 'dock-snap-marker';
|
|
565
|
+
dot.style.left = `${x}px`;
|
|
566
|
+
dot.style.top = `${rootRect.top + sy - rootRect.top}px`;
|
|
567
|
+
layer.appendChild(dot);
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
renderSnapMarkersForCorner() {
|
|
572
|
+
if (!this.showSnapMarkers)
|
|
573
|
+
return;
|
|
574
|
+
const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
|
|
575
|
+
if (!layer)
|
|
576
|
+
return;
|
|
577
|
+
Array.from(layer.querySelectorAll('.dock-snap-marker')).forEach((el) => el.remove());
|
|
578
|
+
if (!this.cornerResizeState)
|
|
579
|
+
return;
|
|
580
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
581
|
+
// Compute representative center lines from first entries
|
|
582
|
+
let centerX = null;
|
|
583
|
+
let centerY = null;
|
|
584
|
+
const st = this.cornerResizeState;
|
|
585
|
+
if (st.vs.length > 0) {
|
|
586
|
+
const vRect = st.vs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
|
|
587
|
+
if (vRect)
|
|
588
|
+
centerX = vRect.left + vRect.width / 2 - rootRect.left;
|
|
589
|
+
}
|
|
590
|
+
if (st.hs.length > 0) {
|
|
591
|
+
const hRect = st.hs[0].container.querySelector(':scope > .dock-split__divider')?.getBoundingClientRect();
|
|
592
|
+
if (hRect)
|
|
593
|
+
centerY = hRect.top + hRect.height / 2 - rootRect.top;
|
|
594
|
+
}
|
|
595
|
+
if (centerY != null) {
|
|
596
|
+
this.cornerSnapXTargets.forEach((sx) => {
|
|
597
|
+
const dot = this.documentRef.createElement('div');
|
|
598
|
+
dot.className = 'dock-snap-marker';
|
|
599
|
+
dot.style.left = `${sx}px`;
|
|
600
|
+
dot.style.top = `${centerY}px`;
|
|
601
|
+
layer.appendChild(dot);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if (centerX != null) {
|
|
605
|
+
this.cornerSnapYTargets.forEach((sy) => {
|
|
606
|
+
const dot = this.documentRef.createElement('div');
|
|
607
|
+
dot.className = 'dock-snap-marker';
|
|
608
|
+
dot.style.left = `${centerX}px`;
|
|
609
|
+
dot.style.top = `${sy}px`;
|
|
610
|
+
layer.appendChild(dot);
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
clearSnapMarkers() {
|
|
615
|
+
if (!this.showSnapMarkers)
|
|
616
|
+
return;
|
|
617
|
+
const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
|
|
618
|
+
if (!layer)
|
|
619
|
+
return;
|
|
620
|
+
Array.from(layer.querySelectorAll('.dock-snap-marker')).forEach((el) => el.remove());
|
|
621
|
+
}
|
|
482
622
|
constructor() {
|
|
483
623
|
super();
|
|
484
624
|
this.dropJoystickTarget = null;
|
|
@@ -490,10 +630,22 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
490
630
|
this.dragState = null;
|
|
491
631
|
this.floatingDragState = null;
|
|
492
632
|
this.floatingResizeState = null;
|
|
633
|
+
this.intersectionRaf = null;
|
|
634
|
+
this.intersectionHandles = new Map();
|
|
635
|
+
this.cornerResizeState = null;
|
|
493
636
|
this.pointerTrackingActive = false;
|
|
494
637
|
this.dragPointerTrackingActive = false;
|
|
495
638
|
this.lastDragPointerPosition = null;
|
|
639
|
+
// Localized snapping while dragging a divider
|
|
640
|
+
this.activeSnapAxis = null;
|
|
641
|
+
this.activeSnapTargets = [];
|
|
642
|
+
// Localized snapping while dragging an intersection handle
|
|
643
|
+
this.cornerSnapXTargets = [];
|
|
644
|
+
this.cornerSnapYTargets = [];
|
|
645
|
+
// Debug: render snap markers while dragging
|
|
646
|
+
this.showSnapMarkers = false;
|
|
496
647
|
this.pendingDragEndTimeout = null;
|
|
648
|
+
this.previousSplitSizes = new Map();
|
|
497
649
|
const documentRef = this.resolveDocument();
|
|
498
650
|
this.documentRef = documentRef;
|
|
499
651
|
this.windowRef = this.resolveWindow(documentRef);
|
|
@@ -543,6 +695,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
543
695
|
this.onDragTouchMove = this.onDragTouchMove.bind(this);
|
|
544
696
|
this.onDragMouseUp = this.onDragMouseUp.bind(this);
|
|
545
697
|
this.onDragTouchEnd = this.onDragTouchEnd.bind(this);
|
|
698
|
+
this.onWindowResize = this.onWindowResize.bind(this);
|
|
546
699
|
}
|
|
547
700
|
connectedCallback() {
|
|
548
701
|
if (!this.hasAttribute('role')) {
|
|
@@ -577,6 +730,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
577
730
|
win?.addEventListener('dragover', this.onGlobalDragOver);
|
|
578
731
|
win?.addEventListener('drag', this.onDrag);
|
|
579
732
|
win?.addEventListener('dragend', this.onGlobalDragEnd, true);
|
|
733
|
+
win?.addEventListener('resize', this.onWindowResize);
|
|
580
734
|
}
|
|
581
735
|
disconnectedCallback() {
|
|
582
736
|
this.rootEl.removeEventListener('dragover', this.onDragOver);
|
|
@@ -593,11 +747,19 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
593
747
|
win?.removeEventListener('pointermove', this.onPointerMove);
|
|
594
748
|
win?.removeEventListener('pointerup', this.onPointerUp);
|
|
595
749
|
this.pointerTrackingActive = false;
|
|
750
|
+
const win2 = this.windowRef;
|
|
751
|
+
win2?.removeEventListener('resize', this.onWindowResize);
|
|
596
752
|
}
|
|
597
753
|
attributeChangedCallback(name, _oldValue, newValue) {
|
|
598
754
|
if (name === 'layout') {
|
|
599
755
|
this.layout = newValue ? this.parseLayout(newValue) : null;
|
|
600
756
|
}
|
|
757
|
+
else if (name === 'debug-snap-markers') {
|
|
758
|
+
this.showSnapMarkers = !(newValue === null || newValue === 'false' || newValue === '0');
|
|
759
|
+
if (!this.showSnapMarkers) {
|
|
760
|
+
this.clearSnapMarkers();
|
|
761
|
+
}
|
|
762
|
+
}
|
|
601
763
|
}
|
|
602
764
|
get layout() {
|
|
603
765
|
return {
|
|
@@ -673,6 +835,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
673
835
|
this.dockedEl.appendChild(fragment);
|
|
674
836
|
}
|
|
675
837
|
this.renderFloatingPanes();
|
|
838
|
+
this.scheduleRenderIntersectionHandles();
|
|
676
839
|
}
|
|
677
840
|
renderNode(node, path, floatingIndex) {
|
|
678
841
|
if (node.kind === 'split') {
|
|
@@ -767,6 +930,479 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
767
930
|
this.floatingLayerEl.appendChild(wrapper);
|
|
768
931
|
});
|
|
769
932
|
}
|
|
933
|
+
onWindowResize() {
|
|
934
|
+
// Recompute intersection handles on window resize
|
|
935
|
+
this.scheduleRenderIntersectionHandles();
|
|
936
|
+
}
|
|
937
|
+
scheduleRenderIntersectionHandles() {
|
|
938
|
+
this.intersectionRaf = null;
|
|
939
|
+
this.renderIntersectionHandles();
|
|
940
|
+
}
|
|
941
|
+
renderIntersectionHandles() {
|
|
942
|
+
const layer = this.shadowRoot?.querySelector('.dock-intersections-layer, .dock-intersection-layer');
|
|
943
|
+
if (!layer)
|
|
944
|
+
return;
|
|
945
|
+
// Keep existing handles; we will diff and update positions
|
|
946
|
+
// 1) Clean up legacy handles (created before keying) that lack a data-key
|
|
947
|
+
Array.from(layer.querySelectorAll('.dock-intersection-handle'))
|
|
948
|
+
.filter((el) => !el.dataset['key'])
|
|
949
|
+
.forEach((el) => el.remove());
|
|
950
|
+
// 2) Rebuild the internal map from DOM to avoid drifting state and dedupe duplicates
|
|
951
|
+
const domByKey = new Map();
|
|
952
|
+
Array.from(layer.querySelectorAll('.dock-intersection-handle[data-key]')).forEach((el) => {
|
|
953
|
+
const key = el.dataset['key'] ?? '';
|
|
954
|
+
if (!key)
|
|
955
|
+
return;
|
|
956
|
+
if (domByKey.has(key)) {
|
|
957
|
+
// Remove duplicates with the same key, keep the first one
|
|
958
|
+
el.remove();
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
domByKey.set(key, el);
|
|
962
|
+
// Ensure listener is attached only once
|
|
963
|
+
if (!el.dataset['listener']) {
|
|
964
|
+
el.dataset['listener'] = '1';
|
|
965
|
+
// Listener will be (re)assigned later when we know the current h/v pair
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
// Sync internal map with DOM
|
|
969
|
+
this.intersectionHandles = domByKey;
|
|
970
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
971
|
+
// If a corner resize is active, only update that handle's position and avoid creating new ones
|
|
972
|
+
if (this.cornerResizeState) {
|
|
973
|
+
const st = this.cornerResizeState;
|
|
974
|
+
const h0 = st.hs[0];
|
|
975
|
+
const v0 = st.vs[0];
|
|
976
|
+
const hPathStr = this.formatPath(h0.path);
|
|
977
|
+
const vPathStr = this.formatPath(v0.path);
|
|
978
|
+
const key = `${hPathStr}:${h0.index}|${vPathStr}:${v0.index}`;
|
|
979
|
+
// Find divider elements corresponding to active paths
|
|
980
|
+
const hDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${hPathStr}"][data-index="${h0.index}"]`);
|
|
981
|
+
const vDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${vPathStr}"][data-index="${v0.index}"]`);
|
|
982
|
+
if (hDiv && vDiv) {
|
|
983
|
+
const hr = hDiv.getBoundingClientRect();
|
|
984
|
+
const vr = vDiv.getBoundingClientRect();
|
|
985
|
+
const x = vr.left + vr.width / 2 - rootRect.left;
|
|
986
|
+
const y = hr.top + hr.height / 2 - rootRect.top;
|
|
987
|
+
const handle = st.handle;
|
|
988
|
+
if (!handle.dataset['key']) {
|
|
989
|
+
handle.dataset['key'] = key;
|
|
990
|
+
}
|
|
991
|
+
this.intersectionHandles.set(key, handle);
|
|
992
|
+
handle.style.left = `${x}px`;
|
|
993
|
+
handle.style.top = `${y}px`;
|
|
994
|
+
// Remove any other handles that don't match the active key
|
|
995
|
+
Array.from(layer.querySelectorAll('.dock-intersection-handle')).forEach((el) => {
|
|
996
|
+
if ((el.dataset['key'] ?? '') !== key) {
|
|
997
|
+
el.remove();
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
// Normalize internal map as well
|
|
1001
|
+
this.intersectionHandles = new Map([[key, handle]]);
|
|
1002
|
+
}
|
|
1003
|
+
return;
|
|
1004
|
+
}
|
|
1005
|
+
const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
|
|
1006
|
+
const hDividers = [];
|
|
1007
|
+
const vDividers = [];
|
|
1008
|
+
allDividers.forEach((el) => {
|
|
1009
|
+
const orientation = el.dataset['orientation'] ?? undefined;
|
|
1010
|
+
const rect = el.getBoundingClientRect();
|
|
1011
|
+
const container = el.closest('.dock-split');
|
|
1012
|
+
const path = this.parsePath(el.dataset['path']);
|
|
1013
|
+
const pathStr = el.dataset['path'] ?? '';
|
|
1014
|
+
const index = Number.parseInt(el.dataset['index'] ?? '', 10);
|
|
1015
|
+
if (!container || !Number.isFinite(index))
|
|
1016
|
+
return;
|
|
1017
|
+
const info = { el, rect, path, pathStr, index, container };
|
|
1018
|
+
// Note: node.direction === 'horizontal' means the split lays out children left-to-right,
|
|
1019
|
+
// which yields a VERTICAL divider bar. So mapping is inverted here.
|
|
1020
|
+
if (orientation === 'horizontal') {
|
|
1021
|
+
vDividers.push(info);
|
|
1022
|
+
}
|
|
1023
|
+
else if (orientation === 'vertical') {
|
|
1024
|
+
hDividers.push(info);
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
const desiredKeys = new Set();
|
|
1028
|
+
const tol = 24; // px tolerance to account for gaps and subpixel layout
|
|
1029
|
+
const groupMap = new Map();
|
|
1030
|
+
const groupPairs = new Map();
|
|
1031
|
+
hDividers.forEach((h) => {
|
|
1032
|
+
const hCenterY = h.rect.top + h.rect.height / 2;
|
|
1033
|
+
vDividers.forEach((v) => {
|
|
1034
|
+
const vCenterX = v.rect.left + v.rect.width / 2;
|
|
1035
|
+
const dx = vCenterX < h.rect.left ? h.rect.left - vCenterX : vCenterX > h.rect.right ? vCenterX - h.rect.right : 0;
|
|
1036
|
+
const dy = hCenterY < v.rect.top ? v.rect.top - hCenterY : hCenterY > v.rect.bottom ? hCenterY - v.rect.bottom : 0;
|
|
1037
|
+
if (dx > tol || dy > tol)
|
|
1038
|
+
return;
|
|
1039
|
+
const x = vCenterX - rootRect.left;
|
|
1040
|
+
const y = hCenterY - rootRect.top;
|
|
1041
|
+
const key = `${h.pathStr}:${h.index}|${v.pathStr}:${v.index}`;
|
|
1042
|
+
const gk = `${Math.round(x)}:${Math.round(y)}`;
|
|
1043
|
+
let handle = groupMap.get(gk);
|
|
1044
|
+
if (!handle) {
|
|
1045
|
+
// Try reuse via existing pair mapping
|
|
1046
|
+
handle = this.intersectionHandles.get(key) ?? null;
|
|
1047
|
+
if (!handle) {
|
|
1048
|
+
handle = this.documentRef.createElement('div');
|
|
1049
|
+
handle.classList.add('dock-intersection-handle', 'glyph');
|
|
1050
|
+
handle.setAttribute('role', 'separator');
|
|
1051
|
+
handle.setAttribute('aria-label', 'Resize split intersection');
|
|
1052
|
+
handle.dataset['key'] = key;
|
|
1053
|
+
handle.dataset['listener'] = '1';
|
|
1054
|
+
handle.addEventListener('pointerdown', (ev) => this.beginCornerResize(ev, h, v, handle));
|
|
1055
|
+
handle.addEventListener('dblclick', (ev) => this.onIntersectionDoubleClick(ev, handle));
|
|
1056
|
+
layer.appendChild(handle);
|
|
1057
|
+
}
|
|
1058
|
+
groupMap.set(gk, handle);
|
|
1059
|
+
}
|
|
1060
|
+
// Track pairs for this group and map all pair keys to the same handle
|
|
1061
|
+
const arr = groupPairs.get(gk) ?? [];
|
|
1062
|
+
arr.push({ h: { pathStr: h.pathStr ?? '', index: h.index }, v: { pathStr: v.pathStr ?? '', index: v.index } });
|
|
1063
|
+
groupPairs.set(gk, arr);
|
|
1064
|
+
this.intersectionHandles.set(key, handle);
|
|
1065
|
+
// Update position for the grouped handle
|
|
1066
|
+
handle.style.left = `${x}px`;
|
|
1067
|
+
handle.style.top = `${y}px`;
|
|
1068
|
+
});
|
|
1069
|
+
});
|
|
1070
|
+
// Attach grouped pairs data to each handle and prune stale ones
|
|
1071
|
+
const keep = new Set(groupMap.values());
|
|
1072
|
+
groupMap.forEach((handle, gk) => {
|
|
1073
|
+
const pairs = groupPairs.get(gk) ?? [];
|
|
1074
|
+
handle.dataset['pairs'] = JSON.stringify(pairs);
|
|
1075
|
+
});
|
|
1076
|
+
Array.from(layer.querySelectorAll('.dock-intersection-handle')).forEach((el) => {
|
|
1077
|
+
if (!keep.has(el)) {
|
|
1078
|
+
el.remove();
|
|
1079
|
+
}
|
|
1080
|
+
});
|
|
1081
|
+
// Reset intersectionHandles to only currently mapped keys
|
|
1082
|
+
const newMap = new Map();
|
|
1083
|
+
groupPairs.forEach((pairs, gk) => {
|
|
1084
|
+
const handle = groupMap.get(gk);
|
|
1085
|
+
pairs.forEach((p) => newMap.set(`${p.h.pathStr}:${p.h.index}|${p.v.pathStr}:${p.v.index}`, handle));
|
|
1086
|
+
});
|
|
1087
|
+
this.intersectionHandles = newMap;
|
|
1088
|
+
}
|
|
1089
|
+
beginCornerResize(event, h, v, handle) {
|
|
1090
|
+
event.preventDefault();
|
|
1091
|
+
// Build pairs from dataset if available (grouped intersections), otherwise from the provided pair
|
|
1092
|
+
const pairsRaw = handle.dataset['pairs'];
|
|
1093
|
+
const parsed = pairsRaw ? JSON.parse(pairsRaw) : [];
|
|
1094
|
+
const hs = [];
|
|
1095
|
+
const vs = [];
|
|
1096
|
+
const ensureHV = (pathStr, index, axis) => {
|
|
1097
|
+
const path = this.parsePath(pathStr);
|
|
1098
|
+
if (!path)
|
|
1099
|
+
return;
|
|
1100
|
+
const div = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${pathStr}"][data-index="${index}"]`) ?? null;
|
|
1101
|
+
const container = div?.closest('.dock-split');
|
|
1102
|
+
if (!container)
|
|
1103
|
+
return;
|
|
1104
|
+
if (axis === 'h') {
|
|
1105
|
+
const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
|
|
1106
|
+
const initial = children.map((c) => c.getBoundingClientRect().height);
|
|
1107
|
+
hs.push({ path, index, container, initialSizes: initial, before: initial[index], after: initial[index + 1] });
|
|
1108
|
+
}
|
|
1109
|
+
else {
|
|
1110
|
+
const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
|
|
1111
|
+
const initial = children.map((c) => c.getBoundingClientRect().width);
|
|
1112
|
+
vs.push({ path, index, container, initialSizes: initial, before: initial[index], after: initial[index + 1] });
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
if (parsed.length > 0) {
|
|
1116
|
+
parsed.forEach((p) => { ensureHV(p.h.pathStr, p.h.index, 'h'); ensureHV(p.v.pathStr, p.v.index, 'v'); });
|
|
1117
|
+
}
|
|
1118
|
+
else if (h.path && v.path) {
|
|
1119
|
+
ensureHV(this.formatPath(h.path), h.index, 'h');
|
|
1120
|
+
ensureHV(this.formatPath(v.path), v.index, 'v');
|
|
1121
|
+
}
|
|
1122
|
+
if (hs.length === 0 && vs.length === 0)
|
|
1123
|
+
return;
|
|
1124
|
+
try {
|
|
1125
|
+
handle.setPointerCapture(event.pointerId);
|
|
1126
|
+
handle.dataset['resizing'] = 'true';
|
|
1127
|
+
handle.classList.add('hovering');
|
|
1128
|
+
}
|
|
1129
|
+
catch { }
|
|
1130
|
+
this.cornerResizeState = {
|
|
1131
|
+
pointerId: event.pointerId,
|
|
1132
|
+
handle,
|
|
1133
|
+
hs: hs.map((e) => ({ path: this.clonePath(e.path), index: e.index, container: e.container, beforeSize: e.before, afterSize: e.after, initialSizes: e.initialSizes, startY: event.clientY })),
|
|
1134
|
+
vs: vs.map((e) => ({ path: this.clonePath(e.path), index: e.index, container: e.container, beforeSize: e.before, afterSize: e.after, initialSizes: e.initialSizes, startX: event.clientX })),
|
|
1135
|
+
};
|
|
1136
|
+
this.startPointerTracking();
|
|
1137
|
+
// Ensure handle has a stable key (use group if present)
|
|
1138
|
+
if (!handle.dataset['key']) {
|
|
1139
|
+
handle.dataset['key'] = handle.dataset['group'] ?? '';
|
|
1140
|
+
}
|
|
1141
|
+
this.renderSnapMarkersForCorner();
|
|
1142
|
+
// Compute localized snap targets for this intersection
|
|
1143
|
+
try {
|
|
1144
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
1145
|
+
// Use first pair to define the crossing lines
|
|
1146
|
+
let centerX = null;
|
|
1147
|
+
let centerY = null;
|
|
1148
|
+
// Resolve one vertical bar (from vs) and one horizontal bar (from hs)
|
|
1149
|
+
if (vs.length > 0) {
|
|
1150
|
+
const vPair = vs[0];
|
|
1151
|
+
const vPathStr = this.formatPath(vPair.path);
|
|
1152
|
+
const vDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${vPathStr}"][data-index="${vPair.index}"]`) ?? null;
|
|
1153
|
+
const vr = vDiv?.getBoundingClientRect();
|
|
1154
|
+
if (vr)
|
|
1155
|
+
centerX = vr.left + vr.width / 2;
|
|
1156
|
+
}
|
|
1157
|
+
if (hs.length > 0) {
|
|
1158
|
+
const hPair = hs[0];
|
|
1159
|
+
const hPathStr = this.formatPath(hPair.path);
|
|
1160
|
+
const hDiv = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${hPathStr}"][data-index="${hPair.index}"]`) ?? null;
|
|
1161
|
+
const hr = hDiv?.getBoundingClientRect();
|
|
1162
|
+
if (hr)
|
|
1163
|
+
centerY = hr.top + hr.height / 2;
|
|
1164
|
+
}
|
|
1165
|
+
const xTargets = [];
|
|
1166
|
+
const yTargets = [];
|
|
1167
|
+
const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
|
|
1168
|
+
allDividers.forEach((el) => {
|
|
1169
|
+
const o = el.dataset['orientation'] ?? undefined;
|
|
1170
|
+
const r = el.getBoundingClientRect();
|
|
1171
|
+
if (o === 'horizontal' && centerY != null) {
|
|
1172
|
+
// vertical bar → contributes X if it crosses centerY
|
|
1173
|
+
if (centerY >= r.top && centerY <= r.bottom) {
|
|
1174
|
+
xTargets.push(r.left + r.width / 2 - rootRect.left);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
else if (o === 'vertical' && centerX != null) {
|
|
1178
|
+
// horizontal bar → contributes Y if it crosses centerX
|
|
1179
|
+
if (centerX >= r.left && centerX <= r.right) {
|
|
1180
|
+
yTargets.push(r.top + r.height / 2 - rootRect.top);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
});
|
|
1184
|
+
this.cornerSnapXTargets = xTargets;
|
|
1185
|
+
this.cornerSnapYTargets = yTargets;
|
|
1186
|
+
}
|
|
1187
|
+
catch {
|
|
1188
|
+
this.cornerSnapXTargets = [];
|
|
1189
|
+
this.cornerSnapYTargets = [];
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
handleCornerResizeMove(event) {
|
|
1193
|
+
const state = this.cornerResizeState;
|
|
1194
|
+
if (!state || state.pointerId !== event.pointerId)
|
|
1195
|
+
return;
|
|
1196
|
+
const snapValue = (val, total, active) => {
|
|
1197
|
+
if (!active || total <= 0)
|
|
1198
|
+
return val;
|
|
1199
|
+
const ratios = [1 / 3, 1 / 2, 2 / 3];
|
|
1200
|
+
const r = val / total;
|
|
1201
|
+
let best = ratios[0];
|
|
1202
|
+
let d = Math.abs(r - best);
|
|
1203
|
+
for (let i = 1; i < ratios.length; i++) {
|
|
1204
|
+
const dd = Math.abs(r - ratios[i]);
|
|
1205
|
+
if (dd < d) {
|
|
1206
|
+
d = dd;
|
|
1207
|
+
best = ratios[i];
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
return best * total;
|
|
1211
|
+
};
|
|
1212
|
+
// Axis snapping to nearby intersections
|
|
1213
|
+
const tol = 10;
|
|
1214
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
1215
|
+
let clientX = event.clientX;
|
|
1216
|
+
let clientY = event.clientY;
|
|
1217
|
+
if (this.cornerSnapXTargets.length) {
|
|
1218
|
+
let best = clientX, bestDist = tol + 1;
|
|
1219
|
+
this.cornerSnapXTargets.forEach((sx) => {
|
|
1220
|
+
const px = rootRect.left + sx;
|
|
1221
|
+
const d = Math.abs(px - clientX);
|
|
1222
|
+
if (d < bestDist) {
|
|
1223
|
+
bestDist = d;
|
|
1224
|
+
best = px;
|
|
1225
|
+
}
|
|
1226
|
+
});
|
|
1227
|
+
if (bestDist <= tol)
|
|
1228
|
+
clientX = best;
|
|
1229
|
+
}
|
|
1230
|
+
if (this.cornerSnapYTargets.length) {
|
|
1231
|
+
let best = clientY, bestDist = tol + 1;
|
|
1232
|
+
this.cornerSnapYTargets.forEach((sy) => {
|
|
1233
|
+
const py = rootRect.top + sy;
|
|
1234
|
+
const d = Math.abs(py - clientY);
|
|
1235
|
+
if (d < bestDist) {
|
|
1236
|
+
bestDist = d;
|
|
1237
|
+
best = py;
|
|
1238
|
+
}
|
|
1239
|
+
});
|
|
1240
|
+
if (bestDist <= tol)
|
|
1241
|
+
clientY = best;
|
|
1242
|
+
}
|
|
1243
|
+
// Update all horizontal bars (vertical splits) with Y delta
|
|
1244
|
+
state.hs.forEach((h) => {
|
|
1245
|
+
const node = this.resolveSplitNode(h.path);
|
|
1246
|
+
if (!node)
|
|
1247
|
+
return;
|
|
1248
|
+
const deltaY = clientY - h.startY;
|
|
1249
|
+
const minSize = 48;
|
|
1250
|
+
const pairTotal = h.beforeSize + h.afterSize;
|
|
1251
|
+
let newBefore = Math.min(Math.max(h.beforeSize + deltaY, minSize), pairTotal - minSize);
|
|
1252
|
+
newBefore = snapValue(newBefore, pairTotal, event.shiftKey);
|
|
1253
|
+
const newAfter = pairTotal - newBefore;
|
|
1254
|
+
const sizesPx = [...h.initialSizes];
|
|
1255
|
+
sizesPx[h.index] = newBefore;
|
|
1256
|
+
sizesPx[h.index + 1] = newAfter;
|
|
1257
|
+
const total = sizesPx.reduce((a, s) => a + s, 0);
|
|
1258
|
+
const normalized = total > 0 ? sizesPx.map((s) => s / total) : [];
|
|
1259
|
+
node.sizes = normalized;
|
|
1260
|
+
const children = Array.from(h.container.querySelectorAll(':scope > .dock-split__child'));
|
|
1261
|
+
normalized.forEach((size, idx) => { if (children[idx])
|
|
1262
|
+
children[idx].style.flex = `${Math.max(size, 0)} 1 0`; });
|
|
1263
|
+
});
|
|
1264
|
+
// Update all vertical bars (horizontal splits) with X delta
|
|
1265
|
+
state.vs.forEach((v) => {
|
|
1266
|
+
const node = this.resolveSplitNode(v.path);
|
|
1267
|
+
if (!node)
|
|
1268
|
+
return;
|
|
1269
|
+
const deltaX = clientX - v.startX;
|
|
1270
|
+
const minSize = 48;
|
|
1271
|
+
const pairTotal = v.beforeSize + v.afterSize;
|
|
1272
|
+
let newBefore = Math.min(Math.max(v.beforeSize + deltaX, minSize), pairTotal - minSize);
|
|
1273
|
+
newBefore = snapValue(newBefore, pairTotal, event.shiftKey);
|
|
1274
|
+
const newAfter = pairTotal - newBefore;
|
|
1275
|
+
const sizesPx = [...v.initialSizes];
|
|
1276
|
+
sizesPx[v.index] = newBefore;
|
|
1277
|
+
sizesPx[v.index + 1] = newAfter;
|
|
1278
|
+
const total = sizesPx.reduce((a, s) => a + s, 0);
|
|
1279
|
+
const normalized = total > 0 ? sizesPx.map((s) => s / total) : [];
|
|
1280
|
+
node.sizes = normalized;
|
|
1281
|
+
const children = Array.from(v.container.querySelectorAll(':scope > .dock-split__child'));
|
|
1282
|
+
normalized.forEach((size, idx) => { if (children[idx])
|
|
1283
|
+
children[idx].style.flex = `${Math.max(size, 0)} 1 0`; });
|
|
1284
|
+
});
|
|
1285
|
+
this.dispatchLayoutChanged();
|
|
1286
|
+
}
|
|
1287
|
+
endCornerResize(pointerId) {
|
|
1288
|
+
const state = this.cornerResizeState;
|
|
1289
|
+
if (!state || pointerId !== state.pointerId)
|
|
1290
|
+
return;
|
|
1291
|
+
try {
|
|
1292
|
+
state.handle.releasePointerCapture(state.pointerId);
|
|
1293
|
+
}
|
|
1294
|
+
catch { }
|
|
1295
|
+
state.handle.dataset['resizing'] = 'false';
|
|
1296
|
+
this.cornerResizeState = null;
|
|
1297
|
+
// Re-render handles to account for new positions
|
|
1298
|
+
this.scheduleRenderIntersectionHandles();
|
|
1299
|
+
this.cornerSnapXTargets = [];
|
|
1300
|
+
this.cornerSnapYTargets = [];
|
|
1301
|
+
}
|
|
1302
|
+
onIntersectionDoubleClick(event, handle) {
|
|
1303
|
+
event.preventDefault();
|
|
1304
|
+
const pairsRaw = handle.dataset['pairs'];
|
|
1305
|
+
const parsed = pairsRaw ? JSON.parse(pairsRaw) : [];
|
|
1306
|
+
if (!Array.isArray(parsed) || parsed.length === 0) {
|
|
1307
|
+
const k = handle.dataset['key'] ?? '';
|
|
1308
|
+
const parts = k.split('|');
|
|
1309
|
+
if (parts.length === 2) {
|
|
1310
|
+
const [hPart, vPart] = parts;
|
|
1311
|
+
const hi = hPart.lastIndexOf(':');
|
|
1312
|
+
const vi = vPart.lastIndexOf(':');
|
|
1313
|
+
if (hi > 0 && vi > 0) {
|
|
1314
|
+
const hPathStr = hPart.slice(0, hi);
|
|
1315
|
+
const vPathStr = vPart.slice(0, vi);
|
|
1316
|
+
const hIdx = Number.parseInt(hPart.slice(hi + 1), 10);
|
|
1317
|
+
const vIdx = Number.parseInt(vPart.slice(vi + 1), 10);
|
|
1318
|
+
parsed.push({ h: { pathStr: hPathStr, index: hIdx }, v: { pathStr: vPathStr, index: vIdx } });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
if (parsed.length === 0)
|
|
1323
|
+
return;
|
|
1324
|
+
const splitKeys = new Set();
|
|
1325
|
+
parsed.forEach((p) => { splitKeys.add(p.h.pathStr); splitKeys.add(p.v.pathStr); });
|
|
1326
|
+
let hasStored = false;
|
|
1327
|
+
splitKeys.forEach((k) => { if (this.previousSplitSizes.has(k))
|
|
1328
|
+
hasStored = true; });
|
|
1329
|
+
const applySizes = (pathStr, mutate) => {
|
|
1330
|
+
const path = this.parsePath(pathStr);
|
|
1331
|
+
if (!path)
|
|
1332
|
+
return;
|
|
1333
|
+
const node = this.resolveSplitNode(path);
|
|
1334
|
+
if (!node)
|
|
1335
|
+
return;
|
|
1336
|
+
const sizes = this.normalizeSizesArray(node.sizes ?? [], node.children.length);
|
|
1337
|
+
// Find divider index from any divider belonging to this path
|
|
1338
|
+
const divEl = this.shadowRoot?.querySelector(`.dock-split__divider[data-path="${pathStr}"]`);
|
|
1339
|
+
const index = divEl ? Number.parseInt(divEl.dataset['index'] ?? '0', 10) : 0;
|
|
1340
|
+
const newSizes = mutate([...sizes], index);
|
|
1341
|
+
node.sizes = newSizes;
|
|
1342
|
+
const segments = path.segments.join('/');
|
|
1343
|
+
const container = this.shadowRoot?.querySelector(`.dock-split[data-path="${segments}"]`);
|
|
1344
|
+
if (container) {
|
|
1345
|
+
const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
|
|
1346
|
+
newSizes.forEach((s, i) => { if (children[i])
|
|
1347
|
+
children[i].style.flex = `${Math.max(s, 0)} 1 0`; });
|
|
1348
|
+
}
|
|
1349
|
+
};
|
|
1350
|
+
if (hasStored) {
|
|
1351
|
+
// Restore stored sizes
|
|
1352
|
+
this.previousSplitSizes.forEach((sizes, pathStr) => {
|
|
1353
|
+
const path = this.parsePath(pathStr);
|
|
1354
|
+
const node = path ? this.resolveSplitNode(path) : null;
|
|
1355
|
+
if (!node)
|
|
1356
|
+
return;
|
|
1357
|
+
const norm = this.normalizeSizesArray(sizes, node.children.length);
|
|
1358
|
+
node.sizes = norm;
|
|
1359
|
+
const segments = path.segments.join('/');
|
|
1360
|
+
const container = this.shadowRoot?.querySelector(`.dock-split[data-path="${segments}"]`);
|
|
1361
|
+
if (container) {
|
|
1362
|
+
const children = Array.from(container.querySelectorAll(':scope > .dock-split__child'));
|
|
1363
|
+
norm.forEach((s, i) => { if (children[i])
|
|
1364
|
+
children[i].style.flex = `${Math.max(s, 0)} 1 0`; });
|
|
1365
|
+
}
|
|
1366
|
+
});
|
|
1367
|
+
this.previousSplitSizes.clear();
|
|
1368
|
+
}
|
|
1369
|
+
else {
|
|
1370
|
+
// Equalize the two panes adjacent to each divider and store previous sizes
|
|
1371
|
+
const touched = new Set();
|
|
1372
|
+
parsed.forEach((p) => {
|
|
1373
|
+
[p.h.pathStr, p.v.pathStr].forEach((key) => {
|
|
1374
|
+
if (touched.has(key))
|
|
1375
|
+
return;
|
|
1376
|
+
const path = this.parsePath(key);
|
|
1377
|
+
const node = path ? this.resolveSplitNode(path) : null;
|
|
1378
|
+
if (node && Array.isArray(node.sizes)) {
|
|
1379
|
+
this.previousSplitSizes.set(key, [...node.sizes]);
|
|
1380
|
+
}
|
|
1381
|
+
touched.add(key);
|
|
1382
|
+
});
|
|
1383
|
+
applySizes(p.h.pathStr, (sizes, idx) => {
|
|
1384
|
+
const total = (sizes[idx] ?? 0) + (sizes[idx + 1] ?? 0);
|
|
1385
|
+
if (total <= 0)
|
|
1386
|
+
return sizes;
|
|
1387
|
+
sizes[idx] = total / 2;
|
|
1388
|
+
sizes[idx + 1] = total / 2;
|
|
1389
|
+
const sum = sizes.reduce((a, s) => a + s, 0);
|
|
1390
|
+
return sum > 0 ? sizes.map((s) => s / sum) : sizes;
|
|
1391
|
+
});
|
|
1392
|
+
applySizes(p.v.pathStr, (sizes, idx) => {
|
|
1393
|
+
const total = (sizes[idx] ?? 0) + (sizes[idx + 1] ?? 0);
|
|
1394
|
+
if (total <= 0)
|
|
1395
|
+
return sizes;
|
|
1396
|
+
sizes[idx] = total / 2;
|
|
1397
|
+
sizes[idx + 1] = total / 2;
|
|
1398
|
+
const sum = sizes.reduce((a, s) => a + s, 0);
|
|
1399
|
+
return sum > 0 ? sizes.map((s) => s / sum) : sizes;
|
|
1400
|
+
});
|
|
1401
|
+
});
|
|
1402
|
+
}
|
|
1403
|
+
this.dispatchLayoutChanged();
|
|
1404
|
+
this.scheduleRenderIntersectionHandles();
|
|
1405
|
+
}
|
|
770
1406
|
beginFloatingDrag(event, index, wrapper, handle) {
|
|
771
1407
|
const floating = this.floatingLayouts[index];
|
|
772
1408
|
if (!floating) {
|
|
@@ -803,6 +1439,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
803
1439
|
event.stopPropagation();
|
|
804
1440
|
try {
|
|
805
1441
|
handle.setPointerCapture(event.pointerId);
|
|
1442
|
+
handle.dataset['resizing'] = 'true';
|
|
806
1443
|
}
|
|
807
1444
|
catch (err) {
|
|
808
1445
|
/* pointer capture may not be supported */
|
|
@@ -848,6 +1485,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
848
1485
|
}
|
|
849
1486
|
try {
|
|
850
1487
|
state.handle.releasePointerCapture(state.pointerId);
|
|
1488
|
+
delete state.handle.dataset['resizing'];
|
|
851
1489
|
}
|
|
852
1490
|
catch (err) {
|
|
853
1491
|
/* no-op */
|
|
@@ -997,7 +1635,8 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
997
1635
|
if (this.pointerTrackingActive &&
|
|
998
1636
|
!this.resizeState &&
|
|
999
1637
|
!this.floatingDragState &&
|
|
1000
|
-
!this.floatingResizeState
|
|
1638
|
+
!this.floatingResizeState &&
|
|
1639
|
+
!this.cornerResizeState) {
|
|
1001
1640
|
const win = this.windowRef;
|
|
1002
1641
|
win?.removeEventListener('pointermove', this.onPointerMove);
|
|
1003
1642
|
win?.removeEventListener('pointerup', this.onPointerUp);
|
|
@@ -1059,6 +1698,13 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1059
1698
|
divider.classList.add('dock-split__divider');
|
|
1060
1699
|
divider.setAttribute('role', 'separator');
|
|
1061
1700
|
divider.tabIndex = 0;
|
|
1701
|
+
// Tag divider with metadata for intersection detection
|
|
1702
|
+
const dividerPath = typeof floatingIndex === 'number'
|
|
1703
|
+
? { type: 'floating', index: floatingIndex, segments: [...path] }
|
|
1704
|
+
: { type: 'docked', segments: [...path] };
|
|
1705
|
+
divider.dataset['path'] = this.formatPath(dividerPath);
|
|
1706
|
+
divider.dataset['index'] = String(index);
|
|
1707
|
+
divider.dataset['orientation'] = node.direction;
|
|
1062
1708
|
divider.addEventListener('pointerdown', (event) => this.beginResize(event, container, floatingIndex !== undefined
|
|
1063
1709
|
? { type: 'floating', index: floatingIndex, segments: [...path] }
|
|
1064
1710
|
: { type: 'docked', segments: [...path] }, index));
|
|
@@ -1183,19 +1829,121 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1183
1829
|
afterSize,
|
|
1184
1830
|
};
|
|
1185
1831
|
this.startPointerTracking();
|
|
1832
|
+
// Compute localized snap targets: intersections with perpendicular dividers near this divider
|
|
1833
|
+
try {
|
|
1834
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
1835
|
+
const dividerRect = divider.getBoundingClientRect();
|
|
1836
|
+
const allDividers = Array.from(this.shadowRoot?.querySelectorAll('.dock-split__divider') ?? []);
|
|
1837
|
+
const targets = [];
|
|
1838
|
+
if (orientation === 'horizontal') {
|
|
1839
|
+
// Current bar is vertical → snap X to centers of other vertical bars (no crossing check needed)
|
|
1840
|
+
allDividers.forEach((el) => {
|
|
1841
|
+
if (el === divider)
|
|
1842
|
+
return;
|
|
1843
|
+
const o = el.dataset['orientation'] ?? undefined;
|
|
1844
|
+
if (o !== 'horizontal')
|
|
1845
|
+
return; // vertical divider bars (split direction horizontal)
|
|
1846
|
+
const r = el.getBoundingClientRect();
|
|
1847
|
+
const xCenter = r.left + r.width / 2 - rootRect.left;
|
|
1848
|
+
targets.push(xCenter);
|
|
1849
|
+
});
|
|
1850
|
+
this.activeSnapAxis = 'x';
|
|
1851
|
+
this.activeSnapTargets = targets;
|
|
1852
|
+
this.renderSnapMarkersForDivider();
|
|
1853
|
+
}
|
|
1854
|
+
else {
|
|
1855
|
+
// Current bar is horizontal → snap Y to centers of other horizontal bars (no crossing check needed)
|
|
1856
|
+
allDividers.forEach((el) => {
|
|
1857
|
+
if (el === divider)
|
|
1858
|
+
return;
|
|
1859
|
+
const o = el.dataset['orientation'] ?? undefined;
|
|
1860
|
+
if (o !== 'vertical')
|
|
1861
|
+
return; // horizontal divider bars (split direction vertical)
|
|
1862
|
+
const r = el.getBoundingClientRect();
|
|
1863
|
+
const yCenter = r.top + r.height / 2 - rootRect.top;
|
|
1864
|
+
targets.push(yCenter);
|
|
1865
|
+
});
|
|
1866
|
+
this.activeSnapAxis = 'y';
|
|
1867
|
+
this.activeSnapTargets = targets;
|
|
1868
|
+
this.renderSnapMarkersForDivider();
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
catch {
|
|
1872
|
+
this.activeSnapAxis = null;
|
|
1873
|
+
this.activeSnapTargets = [];
|
|
1874
|
+
this.clearSnapMarkers();
|
|
1875
|
+
}
|
|
1186
1876
|
}
|
|
1187
1877
|
onPointerMove(event) {
|
|
1878
|
+
if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
|
|
1879
|
+
this.handleCornerResizeMove(event);
|
|
1880
|
+
}
|
|
1188
1881
|
if (this.resizeState && event.pointerId === this.resizeState.pointerId) {
|
|
1189
1882
|
const state = this.resizeState;
|
|
1190
1883
|
const splitNode = this.resolveSplitNode(state.path);
|
|
1191
1884
|
if (!splitNode) {
|
|
1192
1885
|
return;
|
|
1193
1886
|
}
|
|
1194
|
-
|
|
1887
|
+
let currentPos = state.orientation === 'horizontal' ? event.clientX : event.clientY;
|
|
1888
|
+
// Localized axis snap near neighboring intersections
|
|
1889
|
+
const tol = 10;
|
|
1890
|
+
const rootRect = this.rootEl.getBoundingClientRect();
|
|
1891
|
+
if (this.activeSnapTargets.length) {
|
|
1892
|
+
if (state.orientation === 'horizontal' && this.activeSnapAxis === 'x') {
|
|
1893
|
+
// Vertical divider snapping along X
|
|
1894
|
+
let closest = Number.POSITIVE_INFINITY;
|
|
1895
|
+
let best = currentPos;
|
|
1896
|
+
const pointerX = event.clientX;
|
|
1897
|
+
this.activeSnapTargets.forEach((sx) => {
|
|
1898
|
+
const px = rootRect.left + sx;
|
|
1899
|
+
const d = Math.abs(pointerX - px);
|
|
1900
|
+
if (d < closest) {
|
|
1901
|
+
closest = d;
|
|
1902
|
+
best = px;
|
|
1903
|
+
}
|
|
1904
|
+
});
|
|
1905
|
+
if (closest <= tol)
|
|
1906
|
+
currentPos = best;
|
|
1907
|
+
this.renderSnapMarkersForDivider();
|
|
1908
|
+
}
|
|
1909
|
+
else if (state.orientation === 'vertical' && this.activeSnapAxis === 'y') {
|
|
1910
|
+
// Horizontal divider snapping along Y
|
|
1911
|
+
let closest = Number.POSITIVE_INFINITY;
|
|
1912
|
+
let best = currentPos;
|
|
1913
|
+
const pointerY = event.clientY;
|
|
1914
|
+
this.activeSnapTargets.forEach((sy) => {
|
|
1915
|
+
const py = rootRect.top + sy;
|
|
1916
|
+
const d = Math.abs(pointerY - py);
|
|
1917
|
+
if (d < closest) {
|
|
1918
|
+
closest = d;
|
|
1919
|
+
best = py;
|
|
1920
|
+
}
|
|
1921
|
+
});
|
|
1922
|
+
if (closest <= tol)
|
|
1923
|
+
currentPos = best;
|
|
1924
|
+
this.renderSnapMarkersForDivider();
|
|
1925
|
+
}
|
|
1926
|
+
}
|
|
1195
1927
|
const delta = currentPos - state.startPos;
|
|
1196
1928
|
const minSize = 48;
|
|
1197
1929
|
const pairTotal = state.beforeSize + state.afterSize;
|
|
1198
|
-
let newBefore =
|
|
1930
|
+
let newBefore = state.beforeSize + delta;
|
|
1931
|
+
// Optional snap with Shift
|
|
1932
|
+
if (event.shiftKey && pairTotal > 0) {
|
|
1933
|
+
const ratios = [1 / 3, 1 / 2, 2 / 3];
|
|
1934
|
+
const target = newBefore / pairTotal;
|
|
1935
|
+
let best = ratios[0];
|
|
1936
|
+
let bestDist = Math.abs(target - best);
|
|
1937
|
+
for (let i = 1; i < ratios.length; i++) {
|
|
1938
|
+
const d = Math.abs(target - ratios[i]);
|
|
1939
|
+
if (d < bestDist) {
|
|
1940
|
+
best = ratios[i];
|
|
1941
|
+
bestDist = d;
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
newBefore = best * pairTotal;
|
|
1945
|
+
}
|
|
1946
|
+
newBefore = Math.min(Math.max(newBefore, minSize), pairTotal - minSize);
|
|
1199
1947
|
let newAfter = pairTotal - newBefore;
|
|
1200
1948
|
if (!Number.isFinite(newBefore) || !Number.isFinite(newAfter)) {
|
|
1201
1949
|
return;
|
|
@@ -1226,11 +1974,17 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1226
1974
|
}
|
|
1227
1975
|
}
|
|
1228
1976
|
onPointerUp(event) {
|
|
1977
|
+
if (this.cornerResizeState && event.pointerId === this.cornerResizeState.pointerId) {
|
|
1978
|
+
this.endCornerResize(event.pointerId);
|
|
1979
|
+
}
|
|
1229
1980
|
if (this.resizeState && event.pointerId === this.resizeState.pointerId) {
|
|
1230
1981
|
const divider = this.resizeState.divider;
|
|
1231
1982
|
divider.dataset['resizing'] = 'false';
|
|
1232
1983
|
divider.releasePointerCapture(this.resizeState.pointerId);
|
|
1233
1984
|
this.resizeState = null;
|
|
1985
|
+
this.scheduleRenderIntersectionHandles();
|
|
1986
|
+
this.activeSnapAxis = null;
|
|
1987
|
+
this.activeSnapTargets = [];
|
|
1234
1988
|
}
|
|
1235
1989
|
if (this.floatingDragState && event.pointerId === this.floatingDragState.pointerId) {
|
|
1236
1990
|
this.endFloatingDrag(event.pointerId);
|
|
@@ -1341,6 +2095,28 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1341
2095
|
this.startDragPointerTracking();
|
|
1342
2096
|
event.dataTransfer.effectAllowed = 'move';
|
|
1343
2097
|
event.dataTransfer.setData('text/plain', pane);
|
|
2098
|
+
// Preferred UX: if the dragged tab is the only one in its stack,
|
|
2099
|
+
// immediately convert to a floating window unless it is already the
|
|
2100
|
+
// only pane in a floating window (this case is handled by reuse logic).
|
|
2101
|
+
if (this.dragState && this.dragState.floatingIndex !== null && this.dragState.floatingIndex < 0) {
|
|
2102
|
+
const loc = this.resolveStackLocation(this.dragState.sourcePath);
|
|
2103
|
+
if (loc && Array.isArray(loc.node.panes) && loc.node.panes.length === 1) {
|
|
2104
|
+
let shouldConvert = false;
|
|
2105
|
+
if (loc.context === "docked") {
|
|
2106
|
+
shouldConvert = true;
|
|
2107
|
+
}
|
|
2108
|
+
else if (loc.context === "floating") {
|
|
2109
|
+
const floating = this.floatingLayouts[loc.index];
|
|
2110
|
+
const totalPanes = floating && floating.root ? this.countPanesInTree(floating.root) : 0;
|
|
2111
|
+
shouldConvert = totalPanes > 1; // not the only pane in this floating window
|
|
2112
|
+
}
|
|
2113
|
+
if (shouldConvert) {
|
|
2114
|
+
const startX = Number.isFinite(event.clientX) ? event.clientX : (this.dragState.startClientX ?? 0);
|
|
2115
|
+
const startY = Number.isFinite(event.clientY) ? event.clientY : (this.dragState.startClientY ?? 0);
|
|
2116
|
+
this.convertPendingTabDragToFloating(startX, startY);
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
1344
2120
|
}
|
|
1345
2121
|
preparePaneDragSource(path, pane, stackEl, event) {
|
|
1346
2122
|
const location = this.resolveStackLocation(path);
|
|
@@ -1607,6 +2383,8 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1607
2383
|
if (this.dropJoystick.dataset['visible'] !== 'true') {
|
|
1608
2384
|
this.hideDropIndicator();
|
|
1609
2385
|
}
|
|
2386
|
+
// Also ensure any in-header placeholder is cleared when not over a stack
|
|
2387
|
+
this.clearHeaderDragPlaceholder();
|
|
1610
2388
|
return;
|
|
1611
2389
|
}
|
|
1612
2390
|
// If we moved to a different target stack, reset any sticky zone so
|
|
@@ -1617,7 +2395,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1617
2395
|
}
|
|
1618
2396
|
// Previous behavior hid the indicator and returned early here; instead,
|
|
1619
2397
|
// allow the live-reorder branch below to handle in-header drags.
|
|
1620
|
-
// While
|
|
2398
|
+
// While dragging within the same header, show a placeholder and suppress joystick/indicator
|
|
1621
2399
|
if (this.dragState &&
|
|
1622
2400
|
this.dragState.floatingIndex !== null &&
|
|
1623
2401
|
this.dragState.floatingIndex < 0 &&
|
|
@@ -1628,16 +2406,12 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1628
2406
|
if (inHeaderByBounds || inHeaderByHitTest) {
|
|
1629
2407
|
const header = stack.querySelector('.dock-stack__header');
|
|
1630
2408
|
if (header) {
|
|
2409
|
+
// Ensure placeholder exists and move it as the pointer moves
|
|
2410
|
+
this.ensureHeaderDragPlaceholder(header, this.dragState.pane);
|
|
1631
2411
|
const idx = this.computeHeaderInsertIndex(header, clientX);
|
|
1632
2412
|
if (this.dragState.liveReorderIndex !== idx) {
|
|
1633
|
-
this.ensureHeaderDragPlaceholder(header, this.dragState.pane);
|
|
1634
2413
|
this.updateHeaderDragPlaceholderPosition(header, idx);
|
|
1635
|
-
|
|
1636
|
-
if (location) {
|
|
1637
|
-
this.reorderPaneInLocationAtIndex(location, this.dragState.pane, idx);
|
|
1638
|
-
this.render();
|
|
1639
|
-
this.dispatchLayoutChanged();
|
|
1640
|
-
}
|
|
2414
|
+
// Keep model reordering until drop; only move the placeholder now
|
|
1641
2415
|
this.dragState.liveReorderIndex = idx;
|
|
1642
2416
|
}
|
|
1643
2417
|
}
|
|
@@ -1645,6 +2419,8 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1645
2419
|
return;
|
|
1646
2420
|
}
|
|
1647
2421
|
}
|
|
2422
|
+
// Leaving the header: ensure any placeholder is removed immediately
|
|
2423
|
+
this.clearHeaderDragPlaceholder();
|
|
1648
2424
|
const zoneHint = this.findDropZoneByPoint(clientX, clientY);
|
|
1649
2425
|
const zone = this.computeDropZone(stack, { clientX, clientY }, zoneHint);
|
|
1650
2426
|
this.showDropIndicator(stack, zone);
|
|
@@ -1687,11 +2463,14 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1687
2463
|
placeholder.type = 'button';
|
|
1688
2464
|
placeholder.classList.add('dock-tab');
|
|
1689
2465
|
placeholder.dataset['placeholder'] = 'true';
|
|
1690
|
-
placeholder
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
2466
|
+
// Keep the placeholder visually empty but reserving the same width
|
|
2467
|
+
placeholder.textContent = '';
|
|
2468
|
+
placeholder.setAttribute('aria-hidden', 'true');
|
|
2469
|
+
placeholder.style.width = `${dragged.offsetWidth}px`;
|
|
2470
|
+
// Hide the original dragged tab so it doesn't duplicate visually and free up its slot
|
|
2471
|
+
dragged.style.display = 'none';
|
|
2472
|
+
// Insert placeholder in the original position of the dragged tab
|
|
2473
|
+
header.insertBefore(placeholder, dragged);
|
|
1695
2474
|
if (this.dragState) {
|
|
1696
2475
|
this.dragState.placeholderHeader = header;
|
|
1697
2476
|
this.dragState.placeholderEl = placeholder;
|
|
@@ -1703,8 +2482,9 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1703
2482
|
if (!placeholder) {
|
|
1704
2483
|
return;
|
|
1705
2484
|
}
|
|
2485
|
+
const draggedPane = this.dragState?.pane ?? null;
|
|
1706
2486
|
const tabs = Array.from(header.querySelectorAll('.dock-tab'))
|
|
1707
|
-
.filter((t) => t !== placeholder);
|
|
2487
|
+
.filter((t) => t !== placeholder && (!draggedPane || t.dataset['pane'] !== draggedPane));
|
|
1708
2488
|
const clampedTarget = Math.max(0, Math.min(targetIndex, tabs.length));
|
|
1709
2489
|
const ref = tabs[clampedTarget] ?? null;
|
|
1710
2490
|
header.insertBefore(placeholder, ref);
|
|
@@ -1718,7 +2498,7 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1718
2498
|
? (Array.from(header.querySelectorAll('.dock-tab')).find((t) => t.dataset['pane'] === this.dragState?.pane) ?? null)
|
|
1719
2499
|
: null;
|
|
1720
2500
|
if (dragged) {
|
|
1721
|
-
dragged.style.
|
|
2501
|
+
dragged.style.display = '';
|
|
1722
2502
|
}
|
|
1723
2503
|
}
|
|
1724
2504
|
if (ph && ph.parentElement) {
|
|
@@ -1872,20 +2652,41 @@ class MintDockManagerElement extends HTMLElement {
|
|
|
1872
2652
|
this.dispatchLayoutChanged();
|
|
1873
2653
|
}
|
|
1874
2654
|
// Compute the intended tab insert index within a header based on pointer X
|
|
2655
|
+
// Adds a slight rightward bias and uses the placeholder rect (if present)
|
|
2656
|
+
// to ensure offsets are correct even when the dragged tab is display:none.
|
|
1875
2657
|
computeHeaderInsertIndex(header, clientX) {
|
|
1876
|
-
const
|
|
1877
|
-
|
|
1878
|
-
if (tabs.length === 0) {
|
|
2658
|
+
const allTabs = Array.from(header.querySelectorAll('.dock-tab'));
|
|
2659
|
+
if (allTabs.length === 0) {
|
|
1879
2660
|
return 0;
|
|
1880
2661
|
}
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
2662
|
+
const draggedPane = this.dragState?.pane ?? null;
|
|
2663
|
+
const draggedEl = draggedPane
|
|
2664
|
+
? (allTabs.find((t) => t.dataset['pane'] === draggedPane) ?? null)
|
|
2665
|
+
: null;
|
|
2666
|
+
const placeholderEl = header.querySelector('.dock-tab[data-placeholder="true"]');
|
|
2667
|
+
const targets = allTabs.filter((t) => t !== draggedEl && t !== placeholderEl);
|
|
2668
|
+
if (targets.length === 0) {
|
|
2669
|
+
return 0;
|
|
2670
|
+
}
|
|
2671
|
+
const rightBias = 12;
|
|
2672
|
+
const leftBias = 0;
|
|
2673
|
+
const baseRect = placeholderEl
|
|
2674
|
+
? placeholderEl.getBoundingClientRect()
|
|
2675
|
+
: draggedEl
|
|
2676
|
+
? draggedEl.getBoundingClientRect()
|
|
2677
|
+
: null;
|
|
2678
|
+
const rectValid = !!baseRect && Number.isFinite(baseRect.width) && baseRect.width > 0;
|
|
2679
|
+
const draggedCenter = rectValid && baseRect ? baseRect.left + baseRect.width / 2 : null;
|
|
2680
|
+
for (let i = 0; i < targets.length; i += 1) {
|
|
2681
|
+
const rect = targets[i].getBoundingClientRect();
|
|
2682
|
+
const baseMid = rect.left + rect.width / 2;
|
|
2683
|
+
const isRightOfDragged = draggedCenter !== null ? baseMid >= draggedCenter : false;
|
|
2684
|
+
const mid = isRightOfDragged ? baseMid + rightBias : baseMid - leftBias;
|
|
1884
2685
|
if (clientX < mid) {
|
|
1885
2686
|
return i;
|
|
1886
2687
|
}
|
|
1887
2688
|
}
|
|
1888
|
-
return
|
|
2689
|
+
return targets.length;
|
|
1889
2690
|
}
|
|
1890
2691
|
reorderPaneInLocationAtIndex(location, pane, targetIndex) {
|
|
1891
2692
|
const panes = location.node.panes;
|