@mintplayer/ng-bootstrap 21.24.0 → 21.26.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { input, viewChild, TemplateRef, ChangeDetectionStrategy, Component, output, signal, contentChildren, inject, effect, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
2
|
+
import { input, viewChild, TemplateRef, ChangeDetectionStrategy, Component, computed, output, signal, contentChildren, inject, effect, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
3
3
|
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
|
|
4
4
|
import { html, unsafeCSS, LitElement } from 'lit';
|
|
5
5
|
import '@mintplayer/ng-bootstrap/web-components/tab-control';
|
|
@@ -318,12 +318,21 @@ const styles = unsafeCSS(`:host {
|
|
|
318
318
|
padding: 0.5rem 1rem;
|
|
319
319
|
margin: -0.5rem -1rem;
|
|
320
320
|
touch-action: none;
|
|
321
|
+
user-select: none;
|
|
322
|
+
-webkit-user-select: none;
|
|
323
|
+
-webkit-touch-callout: none;
|
|
321
324
|
}
|
|
322
325
|
|
|
323
326
|
.dock-tab:active {
|
|
324
327
|
cursor: grabbing;
|
|
325
328
|
}
|
|
326
329
|
|
|
330
|
+
.dock-tab[data-pressing=true] {
|
|
331
|
+
background-color: var(--bs-secondary-bg-subtle, rgba(0, 0, 0, 0.05));
|
|
332
|
+
transform: scale(1.02);
|
|
333
|
+
transition: background-color 100ms ease-out, transform 100ms ease-out;
|
|
334
|
+
}
|
|
335
|
+
|
|
327
336
|
.dock-stack__pane {
|
|
328
337
|
position: relative;
|
|
329
338
|
display: flex;
|
|
@@ -419,9 +428,16 @@ class MintDockManagerElement extends LitElement {
|
|
|
419
428
|
}
|
|
420
429
|
}
|
|
421
430
|
static get observedAttributes() {
|
|
422
|
-
return [...(super.observedAttributes ?? []), 'layout'];
|
|
431
|
+
return [...(super.observedAttributes ?? []), 'layout', 'debug-layout-integrity'];
|
|
423
432
|
}
|
|
424
433
|
static { this.instanceCounter = 0; }
|
|
434
|
+
// Touch tab-drag gesture: a finger that lands on a tab header must be able
|
|
435
|
+
// to scroll the tabstrip natively. We require a stationary hold before
|
|
436
|
+
// arming the drag, so a horizontal swipe scrolls instead of undocking.
|
|
437
|
+
// See docs/prd/dock-touch-long-press-drag.md.
|
|
438
|
+
static { this.TOUCH_LONG_PRESS_MS = 600; }
|
|
439
|
+
static { this.TOUCH_LONG_PRESS_SLOP_PX = 10; }
|
|
440
|
+
static { this.TOUCH_PRESS_FEEDBACK_DELAY_MS = 150; }
|
|
425
441
|
renderSnapMarkersForCorner() {
|
|
426
442
|
if (!this.showSnapMarkers)
|
|
427
443
|
return;
|
|
@@ -501,6 +517,11 @@ class MintDockManagerElement extends LitElement {
|
|
|
501
517
|
this.cornerSnapYTargets = [];
|
|
502
518
|
// Debug: render snap markers while dragging
|
|
503
519
|
this.showSnapMarkers = false;
|
|
520
|
+
// Debug: assert every pane has a projection slot in shadow DOM after each
|
|
521
|
+
// render. Off by default so production hosts pay no overhead; demo enables
|
|
522
|
+
// it via the `debug-layout-integrity` attribute to catch layout-tree bugs
|
|
523
|
+
// loudly during development.
|
|
524
|
+
this.debugLayoutIntegrity = false;
|
|
504
525
|
this.pendingDragEndTimeout = null;
|
|
505
526
|
this.previousSplitSizes = new Map();
|
|
506
527
|
this.instanceId = `mint-dock-${++MintDockManagerElement.instanceCounter}`;
|
|
@@ -615,6 +636,9 @@ class MintDockManagerElement extends LitElement {
|
|
|
615
636
|
this.clearSnapMarkers();
|
|
616
637
|
}
|
|
617
638
|
}
|
|
639
|
+
else if (name === 'debug-layout-integrity') {
|
|
640
|
+
this.debugLayoutIntegrity = !(newValue === null || newValue === 'false' || newValue === '0');
|
|
641
|
+
}
|
|
618
642
|
}
|
|
619
643
|
get layout() {
|
|
620
644
|
return {
|
|
@@ -657,6 +681,9 @@ class MintDockManagerElement extends LitElement {
|
|
|
657
681
|
this.rootLayout = this.cloneLayoutNode(snapshot.root);
|
|
658
682
|
this.floatingLayouts = this.cloneFloatingArray(snapshot.floating);
|
|
659
683
|
this.titles = snapshot.titles ? { ...snapshot.titles } : {};
|
|
684
|
+
// Sanitize whatever the host fed us: empty stacks, 0/1-child splits, and
|
|
685
|
+
// nested same-direction splits get pruned/flattened before we render.
|
|
686
|
+
this.normalizeAllLayouts();
|
|
660
687
|
this.renderLayout();
|
|
661
688
|
}
|
|
662
689
|
/**
|
|
@@ -740,6 +767,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
740
767
|
// wired up in firstUpdated (rootResizeObserver, dockedMutationObserver,
|
|
741
768
|
// and delegated 'resizing' / 'resize-end' events). The MutationObserver
|
|
742
769
|
// on dockedEl fires when the renderNode subtree above is appended.
|
|
770
|
+
this.verifyProjectionSlots();
|
|
743
771
|
}
|
|
744
772
|
renderNode(node, path, floatingIndex) {
|
|
745
773
|
if (node.kind === 'split') {
|
|
@@ -1814,13 +1842,18 @@ class MintDockManagerElement extends LitElement {
|
|
|
1814
1842
|
this.pendingTabDragMetrics = null;
|
|
1815
1843
|
}
|
|
1816
1844
|
/**
|
|
1817
|
-
* Pointerdown handler arms a "may become a drag" gesture.
|
|
1818
|
-
*
|
|
1819
|
-
* {@link
|
|
1820
|
-
*
|
|
1821
|
-
*
|
|
1845
|
+
* Pointerdown handler arms a "may become a drag" gesture. Mouse / pen use a
|
|
1846
|
+
* 5 px distance threshold and arm immediately; touch dispatches to
|
|
1847
|
+
* {@link armPaneDragGestureTouch} which requires a 600 ms stationary hold
|
|
1848
|
+
* (so the user can scroll the tabstrip natively without undocking).
|
|
1849
|
+
* Once armed, both paths converge on {@link beginPaneDrag}. All listeners
|
|
1850
|
+
* self-clean on resolve so the gesture stays scoped to a single pointerdown.
|
|
1822
1851
|
*/
|
|
1823
1852
|
armPaneDragGesture(startEvent, path, pane, stackEl) {
|
|
1853
|
+
if (startEvent.pointerType === 'touch') {
|
|
1854
|
+
this.armPaneDragGestureTouch(startEvent, path, pane, stackEl);
|
|
1855
|
+
return;
|
|
1856
|
+
}
|
|
1824
1857
|
if (startEvent.pointerType === 'mouse' && startEvent.button !== 0)
|
|
1825
1858
|
return;
|
|
1826
1859
|
const win = this.windowRef;
|
|
@@ -1857,6 +1890,152 @@ class MintDockManagerElement extends LitElement {
|
|
|
1857
1890
|
win.addEventListener('pointerup', onRelease, true);
|
|
1858
1891
|
win.addEventListener('pointercancel', onRelease, true);
|
|
1859
1892
|
}
|
|
1893
|
+
/**
|
|
1894
|
+
* Touch-specific gesture arming. With `.dock-tab` set to `touch-action:
|
|
1895
|
+
* none`, the browser never arbitrates the gesture itself, so JS owns it
|
|
1896
|
+
* from frame 1. Three outcomes from the pending state:
|
|
1897
|
+
*
|
|
1898
|
+
* - User holds within {@link TOUCH_LONG_PRESS_SLOP_PX} for
|
|
1899
|
+
* {@link TOUCH_LONG_PRESS_MS} → timer fires → drag arms via
|
|
1900
|
+
* {@link beginPaneDrag}.
|
|
1901
|
+
* - User moves past slop and the move is mostly horizontal, and the
|
|
1902
|
+
* strip's `<ul>` is scrollable → enter `scrolling` mode and drive
|
|
1903
|
+
* `ul.scrollLeft` from JS for the rest of the gesture (no drag, no
|
|
1904
|
+
* momentum — direct 1:1 finger follow).
|
|
1905
|
+
* - User moves past slop in any other direction, releases, or
|
|
1906
|
+
* pointercancel fires → abandoned, no drag, no scroll. Releases
|
|
1907
|
+
* under slop fire the synthesized click that drives `tab-activate`.
|
|
1908
|
+
*
|
|
1909
|
+
* `touch-action: pan-x` was the original PRD design but doesn't work:
|
|
1910
|
+
* the policy is frozen at touchstart and `setPointerCapture` doesn't
|
|
1911
|
+
* promote it, so first move after a long-press fires pointercancel and
|
|
1912
|
+
* strands the panel. JS-driven scroll is the only model that holds.
|
|
1913
|
+
*/
|
|
1914
|
+
armPaneDragGestureTouch(startEvent, path, pane, stackEl) {
|
|
1915
|
+
const win = this.windowRef;
|
|
1916
|
+
if (!win)
|
|
1917
|
+
return;
|
|
1918
|
+
const startX = startEvent.clientX;
|
|
1919
|
+
const startY = startEvent.clientY;
|
|
1920
|
+
const pointerId = startEvent.pointerId;
|
|
1921
|
+
let latestX = startX;
|
|
1922
|
+
let latestY = startY;
|
|
1923
|
+
let lastScrollX = startX;
|
|
1924
|
+
let resolved = false;
|
|
1925
|
+
let scrolling = false;
|
|
1926
|
+
let pressFeedbackApplied = false;
|
|
1927
|
+
const tabSpan = startEvent.currentTarget instanceof HTMLElement
|
|
1928
|
+
? startEvent.currentTarget
|
|
1929
|
+
: startEvent.target instanceof HTMLElement
|
|
1930
|
+
? startEvent.target.closest('.dock-tab')
|
|
1931
|
+
: null;
|
|
1932
|
+
const stripUl = stackEl
|
|
1933
|
+
? this.getStackStripEl(stackEl)?.querySelector('ul.nav.nav-tabs') ?? null
|
|
1934
|
+
: null;
|
|
1935
|
+
const cleanup = () => {
|
|
1936
|
+
if (resolved)
|
|
1937
|
+
return;
|
|
1938
|
+
resolved = true;
|
|
1939
|
+
win.clearTimeout(longPressTimeout);
|
|
1940
|
+
win.clearTimeout(pressFeedbackTimeout);
|
|
1941
|
+
win.removeEventListener('pointermove', onMove, true);
|
|
1942
|
+
win.removeEventListener('pointerup', onRelease, true);
|
|
1943
|
+
win.removeEventListener('pointercancel', onCancel, true);
|
|
1944
|
+
if (pressFeedbackApplied && tabSpan) {
|
|
1945
|
+
tabSpan.removeAttribute('data-pressing');
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
const onMove = (event) => {
|
|
1949
|
+
if (resolved || event.pointerId !== pointerId)
|
|
1950
|
+
return;
|
|
1951
|
+
if (scrolling) {
|
|
1952
|
+
const dx = event.clientX - lastScrollX;
|
|
1953
|
+
lastScrollX = event.clientX;
|
|
1954
|
+
if (stripUl)
|
|
1955
|
+
stripUl.scrollLeft -= dx;
|
|
1956
|
+
return;
|
|
1957
|
+
}
|
|
1958
|
+
latestX = event.clientX;
|
|
1959
|
+
latestY = event.clientY;
|
|
1960
|
+
const dx = event.clientX - startX;
|
|
1961
|
+
const dy = event.clientY - startY;
|
|
1962
|
+
if (Math.hypot(dx, dy) <= MintDockManagerElement.TOUCH_LONG_PRESS_SLOP_PX) {
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
// Slop crossed before the long-press fired. If the move is mostly
|
|
1966
|
+
// horizontal and the strip can scroll, take over scroll in JS;
|
|
1967
|
+
// otherwise abandon (no drag — browser native UI is fully suppressed
|
|
1968
|
+
// by `touch-action: none` so no fallback is needed).
|
|
1969
|
+
const stripScrollable = !!stripUl && stripUl.scrollWidth > stripUl.clientWidth;
|
|
1970
|
+
if (Math.abs(dx) > Math.abs(dy) && stripScrollable) {
|
|
1971
|
+
win.clearTimeout(longPressTimeout);
|
|
1972
|
+
win.clearTimeout(pressFeedbackTimeout);
|
|
1973
|
+
if (pressFeedbackApplied && tabSpan) {
|
|
1974
|
+
tabSpan.removeAttribute('data-pressing');
|
|
1975
|
+
pressFeedbackApplied = false;
|
|
1976
|
+
}
|
|
1977
|
+
scrolling = true;
|
|
1978
|
+
lastScrollX = event.clientX;
|
|
1979
|
+
stripUl.scrollLeft -= dx;
|
|
1980
|
+
this.clearPendingTabDragMetrics();
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1983
|
+
cleanup();
|
|
1984
|
+
this.clearPendingTabDragMetrics();
|
|
1985
|
+
};
|
|
1986
|
+
const onRelease = (event) => {
|
|
1987
|
+
if (resolved || event.pointerId !== pointerId)
|
|
1988
|
+
return;
|
|
1989
|
+
cleanup();
|
|
1990
|
+
this.clearPendingTabDragMetrics();
|
|
1991
|
+
};
|
|
1992
|
+
const onCancel = (event) => {
|
|
1993
|
+
if (resolved || event.pointerId !== pointerId)
|
|
1994
|
+
return;
|
|
1995
|
+
cleanup();
|
|
1996
|
+
this.clearPendingTabDragMetrics();
|
|
1997
|
+
};
|
|
1998
|
+
const longPressTimeout = win.setTimeout(() => {
|
|
1999
|
+
if (resolved)
|
|
2000
|
+
return;
|
|
2001
|
+
cleanup();
|
|
2002
|
+
try {
|
|
2003
|
+
this.setPointerCapture(pointerId);
|
|
2004
|
+
}
|
|
2005
|
+
catch {
|
|
2006
|
+
// setPointerCapture throws if the pointer is no longer active
|
|
2007
|
+
// (e.g. timer raced a pointerup we didn't see). Drag will abort
|
|
2008
|
+
// naturally on the next event.
|
|
2009
|
+
}
|
|
2010
|
+
if (typeof navigator !== 'undefined' && typeof navigator.vibrate === 'function') {
|
|
2011
|
+
try {
|
|
2012
|
+
navigator.vibrate(10);
|
|
2013
|
+
}
|
|
2014
|
+
catch {
|
|
2015
|
+
// ignore
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
const synthEvent = new PointerEvent('pointermove', {
|
|
2019
|
+
pointerId,
|
|
2020
|
+
pointerType: 'touch',
|
|
2021
|
+
isPrimary: true,
|
|
2022
|
+
clientX: latestX,
|
|
2023
|
+
clientY: latestY,
|
|
2024
|
+
bubbles: false,
|
|
2025
|
+
cancelable: false,
|
|
2026
|
+
});
|
|
2027
|
+
this.beginPaneDrag(synthEvent, path, pane, stackEl);
|
|
2028
|
+
}, MintDockManagerElement.TOUCH_LONG_PRESS_MS);
|
|
2029
|
+
const pressFeedbackTimeout = win.setTimeout(() => {
|
|
2030
|
+
if (resolved || !tabSpan)
|
|
2031
|
+
return;
|
|
2032
|
+
pressFeedbackApplied = true;
|
|
2033
|
+
tabSpan.setAttribute('data-pressing', 'true');
|
|
2034
|
+
}, MintDockManagerElement.TOUCH_PRESS_FEEDBACK_DELAY_MS);
|
|
2035
|
+
win.addEventListener('pointermove', onMove, true);
|
|
2036
|
+
win.addEventListener('pointerup', onRelease, true);
|
|
2037
|
+
win.addEventListener('pointercancel', onCancel, true);
|
|
2038
|
+
}
|
|
1860
2039
|
beginPaneDrag(event, path, pane, stackEl) {
|
|
1861
2040
|
const { path: sourcePath, floatingIndex, pointerOffsetX, pointerOffsetY, } = this.preparePaneDragSource(path, pane, stackEl, event);
|
|
1862
2041
|
// Capture header bounds for detecting when to convert to floating.
|
|
@@ -2420,7 +2599,32 @@ class MintDockManagerElement extends LitElement {
|
|
|
2420
2599
|
const placeholderButton = placeholderTabId
|
|
2421
2600
|
? allTabButtons.find((b) => b.id === `${placeholderTabId}-header-button`) ?? null
|
|
2422
2601
|
: null;
|
|
2423
|
-
|
|
2602
|
+
// The dragged pane's content is `data-hidden`, but mp-tab-control's strip
|
|
2603
|
+
// re-render is async (its MutationObserver schedules a Lit microtask). When
|
|
2604
|
+
// beginPaneDrag calls updateDraggedFloatingPositionFromPoint synchronously
|
|
2605
|
+
// right after preparePaneDragSource, this runs before that re-render — so
|
|
2606
|
+
// the dragged button is still in the strip's shadow. We must exclude it
|
|
2607
|
+
// explicitly, otherwise the loop counts it as a real target and the
|
|
2608
|
+
// placeholder gets appended past the live tabs (visible on touch
|
|
2609
|
+
// long-press, where the finger doesn't move and the user sees the
|
|
2610
|
+
// mis-positioned placeholder for the duration of the hold).
|
|
2611
|
+
const draggedPane = this.dragState?.pane ?? null;
|
|
2612
|
+
let draggedTabId = null;
|
|
2613
|
+
if (draggedPane) {
|
|
2614
|
+
for (const child of Array.from(stack.children)) {
|
|
2615
|
+
if (child instanceof HTMLElement &&
|
|
2616
|
+
child.classList.contains('dock-tab') &&
|
|
2617
|
+
!child.hasAttribute('data-placeholder') &&
|
|
2618
|
+
child.dataset['pane'] === draggedPane) {
|
|
2619
|
+
draggedTabId = child.dataset['tabId'] ?? null;
|
|
2620
|
+
break;
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
}
|
|
2624
|
+
const draggedButton = draggedTabId
|
|
2625
|
+
? allTabButtons.find((b) => b.id === `${draggedTabId}-header-button`) ?? null
|
|
2626
|
+
: null;
|
|
2627
|
+
const targets = allTabButtons.filter((b) => b !== placeholderButton && b !== draggedButton);
|
|
2424
2628
|
if (targets.length === 0) {
|
|
2425
2629
|
return 0;
|
|
2426
2630
|
}
|
|
@@ -2543,16 +2747,14 @@ class MintDockManagerElement extends LitElement {
|
|
|
2543
2747
|
if (!source) {
|
|
2544
2748
|
return;
|
|
2545
2749
|
}
|
|
2546
|
-
|
|
2750
|
+
this.removePaneFromLocation(source, pane, true);
|
|
2547
2751
|
const newRoot = {
|
|
2548
2752
|
kind: 'stack',
|
|
2549
2753
|
panes: [pane],
|
|
2550
2754
|
activePane: pane,
|
|
2551
2755
|
};
|
|
2552
2756
|
this.rootLayout = newRoot;
|
|
2553
|
-
|
|
2554
|
-
this.cleanupLocation(source);
|
|
2555
|
-
}
|
|
2757
|
+
this.normalizeAllLayouts();
|
|
2556
2758
|
this.renderLayout();
|
|
2557
2759
|
this.dispatchLayoutChanged();
|
|
2558
2760
|
if (this.dragState) {
|
|
@@ -2568,6 +2770,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2568
2770
|
return;
|
|
2569
2771
|
}
|
|
2570
2772
|
this.reorderPaneInLocation(source, pane);
|
|
2773
|
+
this.normalizeAllLayouts();
|
|
2571
2774
|
this.renderLayout();
|
|
2572
2775
|
this.dispatchLayoutChanged();
|
|
2573
2776
|
if (this.dragState) {
|
|
@@ -2575,13 +2778,11 @@ class MintDockManagerElement extends LitElement {
|
|
|
2575
2778
|
}
|
|
2576
2779
|
return;
|
|
2577
2780
|
}
|
|
2578
|
-
|
|
2781
|
+
this.removePaneFromLocation(source, pane, true);
|
|
2579
2782
|
if (zone === 'center') {
|
|
2580
2783
|
this.addPaneToLocation(target, pane);
|
|
2581
2784
|
this.setActivePaneForLocation(target, pane);
|
|
2582
|
-
|
|
2583
|
-
this.cleanupLocation(source);
|
|
2584
|
-
}
|
|
2785
|
+
this.normalizeAllLayouts();
|
|
2585
2786
|
this.renderLayout();
|
|
2586
2787
|
this.dispatchLayoutChanged();
|
|
2587
2788
|
if (this.dragState) {
|
|
@@ -2600,9 +2801,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2600
2801
|
else {
|
|
2601
2802
|
const floating = this.floatingLayouts[target.index];
|
|
2602
2803
|
if (!floating) {
|
|
2603
|
-
|
|
2604
|
-
this.cleanupLocation(source);
|
|
2605
|
-
}
|
|
2804
|
+
this.normalizeAllLayouts();
|
|
2606
2805
|
this.renderLayout();
|
|
2607
2806
|
this.dispatchLayoutChanged();
|
|
2608
2807
|
return;
|
|
@@ -2610,9 +2809,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2610
2809
|
floating.root = this.dockNodeBeside(floating.root, target.node, newStack, zone);
|
|
2611
2810
|
floating.activePane = pane;
|
|
2612
2811
|
}
|
|
2613
|
-
|
|
2614
|
-
this.cleanupLocation(source);
|
|
2615
|
-
}
|
|
2812
|
+
this.normalizeAllLayouts();
|
|
2616
2813
|
this.renderLayout();
|
|
2617
2814
|
this.dispatchLayoutChanged();
|
|
2618
2815
|
if (this.dragState) {
|
|
@@ -2630,6 +2827,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2630
2827
|
if (!target && targetPath.type === 'docked' && !this.rootLayout) {
|
|
2631
2828
|
this.rootLayout = this.cloneLayoutNode(source.root);
|
|
2632
2829
|
this.removeFloatingAt(sourceIndex);
|
|
2830
|
+
this.normalizeAllLayouts();
|
|
2633
2831
|
this.renderLayout();
|
|
2634
2832
|
this.dispatchLayoutChanged();
|
|
2635
2833
|
return true;
|
|
@@ -2655,6 +2853,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2655
2853
|
this.setActivePaneForLocation(target, activePane);
|
|
2656
2854
|
}
|
|
2657
2855
|
this.removeFloatingAt(sourceIndex);
|
|
2856
|
+
this.normalizeAllLayouts();
|
|
2658
2857
|
this.renderLayout();
|
|
2659
2858
|
this.dispatchLayoutChanged();
|
|
2660
2859
|
return true;
|
|
@@ -2667,12 +2866,14 @@ class MintDockManagerElement extends LitElement {
|
|
|
2667
2866
|
floating.root = this.dockNodeBeside(floating.root, target.node, source.root, zone);
|
|
2668
2867
|
floating.activePane = source.activePane ?? this.findFirstPaneName(source.root) ?? undefined;
|
|
2669
2868
|
this.removeFloatingAt(sourceIndex);
|
|
2869
|
+
this.normalizeAllLayouts();
|
|
2670
2870
|
this.renderLayout();
|
|
2671
2871
|
this.dispatchLayoutChanged();
|
|
2672
2872
|
return true;
|
|
2673
2873
|
}
|
|
2674
2874
|
this.rootLayout = this.dockNodeBeside(this.rootLayout, target.node, source.root, zone);
|
|
2675
2875
|
this.removeFloatingAt(sourceIndex);
|
|
2876
|
+
this.normalizeAllLayouts();
|
|
2676
2877
|
this.renderLayout();
|
|
2677
2878
|
this.dispatchLayoutChanged();
|
|
2678
2879
|
return true;
|
|
@@ -2710,7 +2911,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
2710
2911
|
if (skipCleanup) {
|
|
2711
2912
|
return true;
|
|
2712
2913
|
}
|
|
2713
|
-
this.
|
|
2914
|
+
this.normalizeAllLayouts();
|
|
2714
2915
|
return true;
|
|
2715
2916
|
}
|
|
2716
2917
|
findParentSplit(node, child) {
|
|
@@ -3083,48 +3284,6 @@ class MintDockManagerElement extends LitElement {
|
|
|
3083
3284
|
this.normalizeSplitNode(parentInfo.parent);
|
|
3084
3285
|
return root;
|
|
3085
3286
|
}
|
|
3086
|
-
cleanupEmptyStackInTree(root, stack) {
|
|
3087
|
-
if (!root || stack.panes.length > 0) {
|
|
3088
|
-
return root;
|
|
3089
|
-
}
|
|
3090
|
-
const parentInfo = this.findParentSplit(root, stack);
|
|
3091
|
-
if (!parentInfo) {
|
|
3092
|
-
return root === stack ? null : root;
|
|
3093
|
-
}
|
|
3094
|
-
const parent = parentInfo.parent;
|
|
3095
|
-
const index = parent.children.indexOf(stack);
|
|
3096
|
-
if (index === -1) {
|
|
3097
|
-
return root;
|
|
3098
|
-
}
|
|
3099
|
-
parent.children.splice(index, 1);
|
|
3100
|
-
if (Array.isArray(parent.sizes)) {
|
|
3101
|
-
parent.sizes.splice(index, 1);
|
|
3102
|
-
}
|
|
3103
|
-
this.normalizeSplitNode(parent);
|
|
3104
|
-
return this.cleanupSplitIfNecessary(root, parent);
|
|
3105
|
-
}
|
|
3106
|
-
cleanupSplitIfNecessary(root, split) {
|
|
3107
|
-
if (split.children.length === 1) {
|
|
3108
|
-
return this.replaceNodeInTree(root, split, split.children[0]);
|
|
3109
|
-
}
|
|
3110
|
-
if (split.children.length === 0) {
|
|
3111
|
-
const parentInfo = this.findParentSplit(root, split);
|
|
3112
|
-
if (!parentInfo) {
|
|
3113
|
-
return null;
|
|
3114
|
-
}
|
|
3115
|
-
const parent = parentInfo.parent;
|
|
3116
|
-
const index = parent.children.indexOf(split);
|
|
3117
|
-
if (index !== -1) {
|
|
3118
|
-
parent.children.splice(index, 1);
|
|
3119
|
-
if (Array.isArray(parent.sizes)) {
|
|
3120
|
-
parent.sizes.splice(index, 1);
|
|
3121
|
-
}
|
|
3122
|
-
this.normalizeSplitNode(parent);
|
|
3123
|
-
return this.cleanupSplitIfNecessary(root, parent);
|
|
3124
|
-
}
|
|
3125
|
-
}
|
|
3126
|
-
return root;
|
|
3127
|
-
}
|
|
3128
3287
|
dockNodeBeside(root, targetNode, newNode, zone) {
|
|
3129
3288
|
const orientation = zone === 'left' || zone === 'right' ? 'horizontal' : 'vertical';
|
|
3130
3289
|
const placeBefore = zone === 'left' || zone === 'top';
|
|
@@ -3345,21 +3504,6 @@ class MintDockManagerElement extends LitElement {
|
|
|
3345
3504
|
}
|
|
3346
3505
|
}
|
|
3347
3506
|
}
|
|
3348
|
-
cleanupLocation(location) {
|
|
3349
|
-
if (location.context === 'docked') {
|
|
3350
|
-
this.rootLayout = this.cleanupEmptyStackInTree(this.rootLayout, location.node);
|
|
3351
|
-
}
|
|
3352
|
-
else {
|
|
3353
|
-
const floating = this.floatingLayouts[location.index];
|
|
3354
|
-
if (!floating) {
|
|
3355
|
-
return;
|
|
3356
|
-
}
|
|
3357
|
-
floating.root = this.cleanupEmptyStackInTree(floating.root, location.node);
|
|
3358
|
-
if (!floating.root) {
|
|
3359
|
-
this.removeFloatingAt(location.index);
|
|
3360
|
-
}
|
|
3361
|
-
}
|
|
3362
|
-
}
|
|
3363
3507
|
reorderPaneInLocation(location, pane) {
|
|
3364
3508
|
const panes = location.node.panes;
|
|
3365
3509
|
const index = panes.indexOf(pane);
|
|
@@ -3415,10 +3559,7 @@ class MintDockManagerElement extends LitElement {
|
|
|
3415
3559
|
if (skipCleanup) {
|
|
3416
3560
|
return true;
|
|
3417
3561
|
}
|
|
3418
|
-
|
|
3419
|
-
if (!floating.root) {
|
|
3420
|
-
this.removeFloatingAt(index);
|
|
3421
|
-
}
|
|
3562
|
+
this.normalizeAllLayouts();
|
|
3422
3563
|
return true;
|
|
3423
3564
|
}
|
|
3424
3565
|
normalizeSizesArray(sizes, count) {
|
|
@@ -3438,6 +3579,115 @@ class MintDockManagerElement extends LitElement {
|
|
|
3438
3579
|
normalizeSplitNode(split) {
|
|
3439
3580
|
split.sizes = this.normalizeSizesArray(split.sizes, split.children.length);
|
|
3440
3581
|
}
|
|
3582
|
+
/**
|
|
3583
|
+
* Bottom-up layout sanitizer. Returns a normalized version of `node` where:
|
|
3584
|
+
* - Empty stacks (panes.length === 0) are dropped (returned as null).
|
|
3585
|
+
* - A stack's `activePane` is repaired if it no longer references one of `panes`.
|
|
3586
|
+
* - Splits whose direction matches a child split are flattened, with sizes
|
|
3587
|
+
* combined multiplicatively so the resulting on-screen pixel layout is
|
|
3588
|
+
* identical to the pre-merge one.
|
|
3589
|
+
* - Splits with 0 children become null. Splits with 1 child are unwrapped.
|
|
3590
|
+
*
|
|
3591
|
+
* Idempotent: passing the result back through this method yields the same
|
|
3592
|
+
* structure. Mutates the input tree in place but only returns nodes that
|
|
3593
|
+
* remain part of the layout.
|
|
3594
|
+
*/
|
|
3595
|
+
normalizeLayoutNode(node) {
|
|
3596
|
+
if (!node)
|
|
3597
|
+
return null;
|
|
3598
|
+
if (node.kind === 'stack') {
|
|
3599
|
+
if (node.panes.length === 0)
|
|
3600
|
+
return null;
|
|
3601
|
+
if (!node.activePane || !node.panes.includes(node.activePane)) {
|
|
3602
|
+
node.activePane = node.panes[0];
|
|
3603
|
+
}
|
|
3604
|
+
return node;
|
|
3605
|
+
}
|
|
3606
|
+
const slotSizes = this.normalizeSizesArray(node.sizes, node.children.length);
|
|
3607
|
+
// Pair each child with its slot weight, drop nulls, then expand any
|
|
3608
|
+
// same-direction child split into its grandchildren with sizes scaled
|
|
3609
|
+
// multiplicatively. A 0.4 slot containing [0.3, 0.7] becomes [0.12, 0.28].
|
|
3610
|
+
const survivors = node.children
|
|
3611
|
+
.map((child, i) => ({ child: this.normalizeLayoutNode(child), slot: slotSizes[i] }))
|
|
3612
|
+
.filter((p) => p.child !== null)
|
|
3613
|
+
.flatMap(({ child, slot }) => {
|
|
3614
|
+
if (child.kind === 'split' && child.direction === node.direction) {
|
|
3615
|
+
const innerSizes = this.normalizeSizesArray(child.sizes, child.children.length);
|
|
3616
|
+
return child.children.map((grandchild, idx) => ({
|
|
3617
|
+
child: grandchild,
|
|
3618
|
+
slot: slot * innerSizes[idx],
|
|
3619
|
+
}));
|
|
3620
|
+
}
|
|
3621
|
+
return [{ child, slot }];
|
|
3622
|
+
});
|
|
3623
|
+
if (survivors.length === 0)
|
|
3624
|
+
return null;
|
|
3625
|
+
if (survivors.length === 1)
|
|
3626
|
+
return survivors[0].child;
|
|
3627
|
+
node.children = survivors.map((s) => s.child);
|
|
3628
|
+
node.sizes = this.normalizeSizesArray(survivors.map((s) => s.slot), survivors.length);
|
|
3629
|
+
return node;
|
|
3630
|
+
}
|
|
3631
|
+
/**
|
|
3632
|
+
* Apply `normalizeLayoutNode` to `rootLayout` and every floating window's
|
|
3633
|
+
* root, drop floating windows whose root collapses to null, and repair
|
|
3634
|
+
* stale `activePane` references on each floating window. Run this at the
|
|
3635
|
+
* end of every public mutation entry point (drop handlers, layout setter,
|
|
3636
|
+
* pane removal) so the tree the renderer sees is always in canonical form.
|
|
3637
|
+
*/
|
|
3638
|
+
normalizeAllLayouts() {
|
|
3639
|
+
this.rootLayout = this.normalizeLayoutNode(this.rootLayout);
|
|
3640
|
+
this.floatingLayouts = this.floatingLayouts
|
|
3641
|
+
.map((floating) => {
|
|
3642
|
+
floating.root = this.normalizeLayoutNode(floating.root);
|
|
3643
|
+
if (!floating.root)
|
|
3644
|
+
return null;
|
|
3645
|
+
const panes = this.collectPaneNames(floating.root);
|
|
3646
|
+
if (!floating.activePane || !panes.includes(floating.activePane)) {
|
|
3647
|
+
const fallback = this.findFirstPaneName(floating.root);
|
|
3648
|
+
if (fallback) {
|
|
3649
|
+
floating.activePane = fallback;
|
|
3650
|
+
}
|
|
3651
|
+
else {
|
|
3652
|
+
delete floating.activePane;
|
|
3653
|
+
}
|
|
3654
|
+
}
|
|
3655
|
+
return floating;
|
|
3656
|
+
})
|
|
3657
|
+
.filter((f) => f !== null);
|
|
3658
|
+
}
|
|
3659
|
+
/**
|
|
3660
|
+
* Dev-mode integrity guard: walks every pane referenced by the current
|
|
3661
|
+
* layout and asserts that the rendered shadow DOM contains a matching
|
|
3662
|
+
* `<slot name="${pane}">`. A missing slot means the layout tree got into
|
|
3663
|
+
* a state the renderer can't display — typically a missed normalize() call
|
|
3664
|
+
* or a render bug. Opt in via the `debug-layout-integrity` attribute or
|
|
3665
|
+
* the `debugLayoutIntegrity` property; off by default.
|
|
3666
|
+
*/
|
|
3667
|
+
verifyProjectionSlots() {
|
|
3668
|
+
if (!this.debugLayoutIntegrity)
|
|
3669
|
+
return;
|
|
3670
|
+
const root = this.shadowRoot;
|
|
3671
|
+
if (!root)
|
|
3672
|
+
return;
|
|
3673
|
+
// Collect every slot the renderer produced. Walking the rendered DOM
|
|
3674
|
+
// (instead of building a CSS selector per pane) sidesteps environment
|
|
3675
|
+
// differences — e.g. jsdom does not expose `CSS.escape`, which would
|
|
3676
|
+
// crash the guard during unit tests before the assertion runs.
|
|
3677
|
+
const slotNames = new Set(Array.from(root.querySelectorAll('slot'))
|
|
3678
|
+
.map((slot) => slot.getAttribute('name'))
|
|
3679
|
+
.filter((name) => !!name));
|
|
3680
|
+
const panes = [
|
|
3681
|
+
...this.collectPaneNames(this.rootLayout),
|
|
3682
|
+
...this.floatingLayouts.flatMap((f) => this.collectPaneNames(f.root)),
|
|
3683
|
+
];
|
|
3684
|
+
const missing = panes.find((pane) => !slotNames.has(pane));
|
|
3685
|
+
if (missing) {
|
|
3686
|
+
throw new Error(`mint-dock-manager: pane "${missing}" has no projection slot in the shadow DOM. ` +
|
|
3687
|
+
`The layout tree got into a state the renderer can't display — likely a ` +
|
|
3688
|
+
`missing normalize() call or a render bug.`);
|
|
3689
|
+
}
|
|
3690
|
+
}
|
|
3441
3691
|
dispatchLayoutChanged() {
|
|
3442
3692
|
this.dispatchEvent(new CustomEvent('dock-layout-changed', {
|
|
3443
3693
|
detail: this.snapshot,
|
|
@@ -3469,6 +3719,14 @@ class BsDockManagerComponent {
|
|
|
3469
3719
|
}
|
|
3470
3720
|
constructor() {
|
|
3471
3721
|
this.layout = input(null, ...(ngDevMode ? [{ debugName: "layout" }] : /* istanbul ignore next */ []));
|
|
3722
|
+
/**
|
|
3723
|
+
* Dev-mode integrity guard. When `true`, the inner web component throws
|
|
3724
|
+
* after each render if any registered pane has no projection slot in the
|
|
3725
|
+
* shadow DOM — a signal that the layout tree got corrupted. Off by default;
|
|
3726
|
+
* enable in development to catch layout-logic bugs loudly.
|
|
3727
|
+
*/
|
|
3728
|
+
this.debugLayoutIntegrity = input(false, ...(ngDevMode ? [{ debugName: "debugLayoutIntegrity" }] : /* istanbul ignore next */ []));
|
|
3729
|
+
this.debugLayoutIntegrityAttr = computed(() => this.debugLayoutIntegrity() ? '' : null, ...(ngDevMode ? [{ debugName: "debugLayoutIntegrityAttr" }] : /* istanbul ignore next */ []));
|
|
3472
3730
|
this.layoutChange = output();
|
|
3473
3731
|
this.layoutSnapshotChange = output();
|
|
3474
3732
|
this.layoutString = signal(null, ...(ngDevMode ? [{ debugName: "layoutString" }] : /* istanbul ignore next */ []));
|
|
@@ -3537,12 +3795,12 @@ class BsDockManagerComponent {
|
|
|
3537
3795
|
return JSON.parse(JSON.stringify(layout));
|
|
3538
3796
|
}
|
|
3539
3797
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
3540
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: BsDockManagerComponent, isStandalone: true, selector: "bs-dock-manager", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange", layoutSnapshotChange: "layoutSnapshotChange" }, queries: [{ propertyName: "panes", predicate: BsDockPaneComponent, isSignal: true }], viewQueries: [{ propertyName: "managerRef", first: true, predicate: ["manager"], descendants: true, isSignal: true }], ngImport: i0, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3798
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.11", type: BsDockManagerComponent, isStandalone: true, selector: "bs-dock-manager", inputs: { layout: { classPropertyName: "layout", publicName: "layout", isSignal: true, isRequired: false, transformFunction: null }, debugLayoutIntegrity: { classPropertyName: "debugLayoutIntegrity", publicName: "debugLayoutIntegrity", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { layoutChange: "layoutChange", layoutSnapshotChange: "layoutSnapshotChange" }, queries: [{ propertyName: "panes", predicate: BsDockPaneComponent, isSignal: true }], viewQueries: [{ propertyName: "managerRef", first: true, predicate: ["manager"], descendants: true, isSignal: true }], ngImport: i0, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n [attr.debug-layout-integrity]=\"debugLayoutIntegrityAttr()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"], dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
3541
3799
|
}
|
|
3542
3800
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.11", ngImport: i0, type: BsDockManagerComponent, decorators: [{
|
|
3543
3801
|
type: Component,
|
|
3544
|
-
args: [{ selector: 'bs-dock-manager', imports: [NgTemplateOutlet], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"] }]
|
|
3545
|
-
}], ctorParameters: () => [], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], layoutChange: [{ type: i0.Output, args: ["layoutChange"] }], layoutSnapshotChange: [{ type: i0.Output, args: ["layoutSnapshotChange"] }], panes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => BsDockPaneComponent), { isSignal: true }] }], managerRef: [{ type: i0.ViewChild, args: ['manager', { isSignal: true }] }] } });
|
|
3802
|
+
args: [{ selector: 'bs-dock-manager', imports: [NgTemplateOutlet], schemas: [CUSTOM_ELEMENTS_SCHEMA], changeDetection: ChangeDetectionStrategy.OnPush, template: "<mint-dock-manager\n #manager\n class=\"bs-dock-manager\"\n [attr.layout]=\"layoutString()\"\n [attr.debug-layout-integrity]=\"debugLayoutIntegrityAttr()\"\n (dock-layout-changed)=\"onLayoutChanged($event)\"\n >\n @for (pane of panes(); track trackByPane($index, pane)) {\n <div class=\"bs-dock-pane\" [attr.slot]=\"pane.name()\">\n <ng-container *ngTemplateOutlet=\"pane.template()\"></ng-container>\n </div>\n }\n</mint-dock-manager>\n", styles: [":host{display:block;width:100%;height:100%}.bs-dock-manager{display:block;width:100%;height:100%}.bs-dock-pane{display:contents}\n"] }]
|
|
3803
|
+
}], ctorParameters: () => [], propDecorators: { layout: [{ type: i0.Input, args: [{ isSignal: true, alias: "layout", required: false }] }], debugLayoutIntegrity: [{ type: i0.Input, args: [{ isSignal: true, alias: "debugLayoutIntegrity", required: false }] }], layoutChange: [{ type: i0.Output, args: ["layoutChange"] }], layoutSnapshotChange: [{ type: i0.Output, args: ["layoutSnapshotChange"] }], panes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => BsDockPaneComponent), { isSignal: true }] }], managerRef: [{ type: i0.ViewChild, args: ['manager', { isSignal: true }] }] } });
|
|
3546
3804
|
|
|
3547
3805
|
/**
|
|
3548
3806
|
* Generated bundle index. Do not edit.
|