@cdevhub/ngx-tw 0.2.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/LICENSE +21 -0
- package/README.md +134 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs +108 -0
- package/fesm2022/cdevhub-ngx-tw-accordion.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs +240 -0
- package/fesm2022/cdevhub-ngx-tw-alert.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs +287 -0
- package/fesm2022/cdevhub-ngx-tw-avatar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs +332 -0
- package/fesm2022/cdevhub-ngx-tw-badge.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs +452 -0
- package/fesm2022/cdevhub-ngx-tw-breadcrumbs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs +227 -0
- package/fesm2022/cdevhub-ngx-tw-button.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs +417 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-luxon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs +232 -0
- package/fesm2022/cdevhub-ngx-tw-calendar-testing.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs +4482 -0
- package/fesm2022/cdevhub-ngx-tw-calendar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs +157 -0
- package/fesm2022/cdevhub-ngx-tw-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs +1454 -0
- package/fesm2022/cdevhub-ngx-tw-carousel.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs +593 -0
- package/fesm2022/cdevhub-ngx-tw-checkbox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs +228 -0
- package/fesm2022/cdevhub-ngx-tw-code-block.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs +510 -0
- package/fesm2022/cdevhub-ngx-tw-collapsible.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs +1721 -0
- package/fesm2022/cdevhub-ngx-tw-combobox.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs +721 -0
- package/fesm2022/cdevhub-ngx-tw-command-palette.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs +861 -0
- package/fesm2022/cdevhub-ngx-tw-core.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs +1558 -0
- package/fesm2022/cdevhub-ngx-tw-date-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs +1833 -0
- package/fesm2022/cdevhub-ngx-tw-date-range-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs +693 -0
- package/fesm2022/cdevhub-ngx-tw-dialog.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs +331 -0
- package/fesm2022/cdevhub-ngx-tw-empty-state.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs +289 -0
- package/fesm2022/cdevhub-ngx-tw-flip-card.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs +572 -0
- package/fesm2022/cdevhub-ngx-tw-form-field.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs +36 -0
- package/fesm2022/cdevhub-ngx-tw-icon-lucide.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs +278 -0
- package/fesm2022/cdevhub-ngx-tw-icon.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs +355 -0
- package/fesm2022/cdevhub-ngx-tw-input.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs +241 -0
- package/fesm2022/cdevhub-ngx-tw-item.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs +433 -0
- package/fesm2022/cdevhub-ngx-tw-menu.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs +691 -0
- package/fesm2022/cdevhub-ngx-tw-paginator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs +678 -0
- package/fesm2022/cdevhub-ngx-tw-popover.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs +335 -0
- package/fesm2022/cdevhub-ngx-tw-progress-bar.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs +708 -0
- package/fesm2022/cdevhub-ngx-tw-radio.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs +319 -0
- package/fesm2022/cdevhub-ngx-tw-segmented-control.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs +1722 -0
- package/fesm2022/cdevhub-ngx-tw-select.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs +115 -0
- package/fesm2022/cdevhub-ngx-tw-separator.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs +768 -0
- package/fesm2022/cdevhub-ngx-tw-sheet.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs +149 -0
- package/fesm2022/cdevhub-ngx-tw-skeleton.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs +1147 -0
- package/fesm2022/cdevhub-ngx-tw-slider.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs +346 -0
- package/fesm2022/cdevhub-ngx-tw-sort.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs +186 -0
- package/fesm2022/cdevhub-ngx-tw-spinner.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs +1082 -0
- package/fesm2022/cdevhub-ngx-tw-split.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs +579 -0
- package/fesm2022/cdevhub-ngx-tw-stat.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs +443 -0
- package/fesm2022/cdevhub-ngx-tw-stepper.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs +391 -0
- package/fesm2022/cdevhub-ngx-tw-switch.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs +315 -0
- package/fesm2022/cdevhub-ngx-tw-tab-nav.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs +994 -0
- package/fesm2022/cdevhub-ngx-tw-table.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs +508 -0
- package/fesm2022/cdevhub-ngx-tw-tabs.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs +147 -0
- package/fesm2022/cdevhub-ngx-tw-textarea.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs +178 -0
- package/fesm2022/cdevhub-ngx-tw-theme.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs +1344 -0
- package/fesm2022/cdevhub-ngx-tw-time-picker.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs +1136 -0
- package/fesm2022/cdevhub-ngx-tw-timeline.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs +1311 -0
- package/fesm2022/cdevhub-ngx-tw-toast.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs +533 -0
- package/fesm2022/cdevhub-ngx-tw-tooltip.mjs.map +1 -0
- package/fesm2022/cdevhub-ngx-tw.mjs +61 -0
- package/fesm2022/cdevhub-ngx-tw.mjs.map +1 -0
- package/package.json +265 -0
- package/theme/_base.css +394 -0
- package/theme/_dark.css +510 -0
- package/theme/_high-contrast.css +290 -0
- package/theme/_semantic.css +317 -0
- package/theme/_typography.css +9 -0
- package/theme/index.css +54 -0
- package/types/cdevhub-ngx-tw-accordion.d.ts +48 -0
- package/types/cdevhub-ngx-tw-alert.d.ts +67 -0
- package/types/cdevhub-ngx-tw-avatar.d.ts +79 -0
- package/types/cdevhub-ngx-tw-badge.d.ts +94 -0
- package/types/cdevhub-ngx-tw-breadcrumbs.d.ts +227 -0
- package/types/cdevhub-ngx-tw-button.d.ts +74 -0
- package/types/cdevhub-ngx-tw-calendar-luxon.d.ts +200 -0
- package/types/cdevhub-ngx-tw-calendar-testing.d.ts +113 -0
- package/types/cdevhub-ngx-tw-calendar.d.ts +1704 -0
- package/types/cdevhub-ngx-tw-card.d.ts +48 -0
- package/types/cdevhub-ngx-tw-carousel.d.ts +406 -0
- package/types/cdevhub-ngx-tw-checkbox.d.ts +136 -0
- package/types/cdevhub-ngx-tw-code-block.d.ts +93 -0
- package/types/cdevhub-ngx-tw-collapsible.d.ts +151 -0
- package/types/cdevhub-ngx-tw-combobox.d.ts +389 -0
- package/types/cdevhub-ngx-tw-command-palette.d.ts +215 -0
- package/types/cdevhub-ngx-tw-core.d.ts +719 -0
- package/types/cdevhub-ngx-tw-date-picker.d.ts +317 -0
- package/types/cdevhub-ngx-tw-date-range-picker.d.ts +313 -0
- package/types/cdevhub-ngx-tw-dialog.d.ts +303 -0
- package/types/cdevhub-ngx-tw-empty-state.d.ts +104 -0
- package/types/cdevhub-ngx-tw-flip-card.d.ts +95 -0
- package/types/cdevhub-ngx-tw-form-field.d.ts +210 -0
- package/types/cdevhub-ngx-tw-icon-lucide.d.ts +29 -0
- package/types/cdevhub-ngx-tw-icon.d.ts +134 -0
- package/types/cdevhub-ngx-tw-input.d.ts +110 -0
- package/types/cdevhub-ngx-tw-item.d.ts +70 -0
- package/types/cdevhub-ngx-tw-menu.d.ts +99 -0
- package/types/cdevhub-ngx-tw-paginator.d.ts +305 -0
- package/types/cdevhub-ngx-tw-popover.d.ts +157 -0
- package/types/cdevhub-ngx-tw-progress-bar.d.ts +100 -0
- package/types/cdevhub-ngx-tw-radio.d.ts +174 -0
- package/types/cdevhub-ngx-tw-segmented-control.d.ts +66 -0
- package/types/cdevhub-ngx-tw-select.d.ts +384 -0
- package/types/cdevhub-ngx-tw-separator.d.ts +28 -0
- package/types/cdevhub-ngx-tw-sheet.d.ts +335 -0
- package/types/cdevhub-ngx-tw-skeleton.d.ts +52 -0
- package/types/cdevhub-ngx-tw-slider.d.ts +258 -0
- package/types/cdevhub-ngx-tw-sort.d.ts +136 -0
- package/types/cdevhub-ngx-tw-spinner.d.ts +50 -0
- package/types/cdevhub-ngx-tw-split.d.ts +265 -0
- package/types/cdevhub-ngx-tw-stat.d.ts +140 -0
- package/types/cdevhub-ngx-tw-stepper.d.ts +127 -0
- package/types/cdevhub-ngx-tw-switch.d.ts +88 -0
- package/types/cdevhub-ngx-tw-tab-nav.d.ts +107 -0
- package/types/cdevhub-ngx-tw-table.d.ts +479 -0
- package/types/cdevhub-ngx-tw-tabs.d.ts +121 -0
- package/types/cdevhub-ngx-tw-textarea.d.ts +62 -0
- package/types/cdevhub-ngx-tw-theme.d.ts +106 -0
- package/types/cdevhub-ngx-tw-time-picker.d.ts +303 -0
- package/types/cdevhub-ngx-tw-timeline.d.ts +278 -0
- package/types/cdevhub-ngx-tw-toast.d.ts +388 -0
- package/types/cdevhub-ngx-tw-tooltip.d.ts +95 -0
- package/types/cdevhub-ngx-tw.d.ts +49 -0
|
@@ -0,0 +1,1082 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { input, output, signal, untracked, ChangeDetectionStrategy, Component, inject, ElementRef, NgZone, DestroyRef, PLATFORM_ID, contentChildren, computed, afterNextRender, effect, isDevMode, Directive } from '@angular/core';
|
|
3
|
+
import { isPlatformBrowser } from '@angular/common';
|
|
4
|
+
import { Directionality } from '@angular/cdk/bidi';
|
|
5
|
+
import { tv } from 'tailwind-variants';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* A single resizable pane inside `<tw-split>`.
|
|
9
|
+
* Declare size constraints here; the parent container drives the actual sizing.
|
|
10
|
+
*/
|
|
11
|
+
class SplitPaneComponent {
|
|
12
|
+
/** Initial size of this pane in the container's unit. Omit to use even distribution. */
|
|
13
|
+
defaultSize = input(undefined, ...(ngDevMode ? [{ debugName: "defaultSize" }] : /* istanbul ignore next */ []));
|
|
14
|
+
/** Minimum size in the container's unit. The gutter will not move past this. Defaults to `0`. */
|
|
15
|
+
minSize = input(0, ...(ngDevMode ? [{ debugName: "minSize" }] : /* istanbul ignore next */ []));
|
|
16
|
+
/** Maximum size in the container's unit. The gutter will not move past this. Defaults to `Infinity`. */
|
|
17
|
+
maxSize = input(Infinity, ...(ngDevMode ? [{ debugName: "maxSize" }] : /* istanbul ignore next */ []));
|
|
18
|
+
/** When true, the pane may collapse to `collapsedSize` via snap, keyboard, or API. Defaults to `false`. */
|
|
19
|
+
collapsible = input(false, ...(ngDevMode ? [{ debugName: "collapsible" }] : /* istanbul ignore next */ []));
|
|
20
|
+
/** Size to use when the pane is collapsed. May be `> 0` for rail-style collapse. Defaults to `0`. */
|
|
21
|
+
collapsedSize = input(0, ...(ngDevMode ? [{ debugName: "collapsedSize" }] : /* istanbul ignore next */ []));
|
|
22
|
+
/**
|
|
23
|
+
* If `> 0`, dragging within `snapSize` units of `collapsedSize` snaps the pane closed.
|
|
24
|
+
* Dragging back out past `snapSize` re-expands it. Defaults to `0` (snap disabled).
|
|
25
|
+
*/
|
|
26
|
+
snapSize = input(0, ...(ngDevMode ? [{ debugName: "snapSize" }] : /* istanbul ignore next */ []));
|
|
27
|
+
/**
|
|
28
|
+
* Stable ordering token used by the container when content-projection order and
|
|
29
|
+
* resize math must stay consistent across change detection. Defaults to declaration order.
|
|
30
|
+
*/
|
|
31
|
+
order = input(undefined, ...(ngDevMode ? [{ debugName: "order" }] : /* istanbul ignore next */ []));
|
|
32
|
+
/** Fires when this pane's size changes, in the container's unit. */
|
|
33
|
+
sizeChange = output();
|
|
34
|
+
/** Fires when this pane's collapsed state changes. */
|
|
35
|
+
collapsedChange = output();
|
|
36
|
+
/**
|
|
37
|
+
* @internal
|
|
38
|
+
* Written by the parent SplitComponent to set this pane's current flex-basis.
|
|
39
|
+
* Not part of the public API.
|
|
40
|
+
*/
|
|
41
|
+
_basis = signal('', ...(ngDevMode ? [{ debugName: "_basis" }] : /* istanbul ignore next */ []));
|
|
42
|
+
/**
|
|
43
|
+
* @internal
|
|
44
|
+
* Current size in the container's unit. Written by the parent SplitComponent.
|
|
45
|
+
* Not part of the public API.
|
|
46
|
+
*/
|
|
47
|
+
_size = signal(0, ...(ngDevMode ? [{ debugName: "_size" }] : /* istanbul ignore next */ []));
|
|
48
|
+
/**
|
|
49
|
+
* @internal
|
|
50
|
+
* Written by the parent SplitComponent to reflect collapsed state on the host.
|
|
51
|
+
* Not part of the public API.
|
|
52
|
+
*/
|
|
53
|
+
_collapsed = signal(false, ...(ngDevMode ? [{ debugName: "_collapsed" }] : /* istanbul ignore next */ []));
|
|
54
|
+
/**
|
|
55
|
+
* @internal
|
|
56
|
+
* Size recorded immediately before this pane collapses, restored on expand.
|
|
57
|
+
* `null` whenever the pane is not collapsed or no pre-collapse size was captured.
|
|
58
|
+
*/
|
|
59
|
+
_preCollapseSize = signal(null, ...(ngDevMode ? [{ debugName: "_preCollapseSize" }] : /* istanbul ignore next */ []));
|
|
60
|
+
/**
|
|
61
|
+
* @internal
|
|
62
|
+
* Pane's positional index. Drives flex `order` so gutters can interleave via
|
|
63
|
+
* order = index * 2 + 1.
|
|
64
|
+
*/
|
|
65
|
+
_index = signal(0, ...(ngDevMode ? [{ debugName: "_index" }] : /* istanbul ignore next */ []));
|
|
66
|
+
/**
|
|
67
|
+
* @internal
|
|
68
|
+
* DOM id used by the parent gutter's `aria-controls` reference.
|
|
69
|
+
* Assigned by the parent SplitComponent on content reconciliation.
|
|
70
|
+
*/
|
|
71
|
+
_paneId = signal(null, ...(ngDevMode ? [{ debugName: "_paneId" }] : /* istanbul ignore next */ []));
|
|
72
|
+
/**
|
|
73
|
+
* @internal — idempotent id assignment. Re-using the existing id when the
|
|
74
|
+
* pane is re-reconciled keeps aria-controls stable across CD. The current
|
|
75
|
+
* id is read via `untracked` so callers running inside an `effect()` don't
|
|
76
|
+
* subscribe to `_paneId` and re-fire when it transitions from `null`.
|
|
77
|
+
*/
|
|
78
|
+
_ensureId(componentId, index) {
|
|
79
|
+
if (untracked(() => this._paneId()) === null) {
|
|
80
|
+
this._paneId.set(`${componentId}-pane-${index}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitPaneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
84
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.1.0", version: "21.2.14", type: SplitPaneComponent, isStandalone: true, selector: "tw-split-pane", inputs: { defaultSize: { classPropertyName: "defaultSize", publicName: "defaultSize", isSignal: true, isRequired: false, transformFunction: null }, minSize: { classPropertyName: "minSize", publicName: "minSize", isSignal: true, isRequired: false, transformFunction: null }, maxSize: { classPropertyName: "maxSize", publicName: "maxSize", isSignal: true, isRequired: false, transformFunction: null }, collapsible: { classPropertyName: "collapsible", publicName: "collapsible", isSignal: true, isRequired: false, transformFunction: null }, collapsedSize: { classPropertyName: "collapsedSize", publicName: "collapsedSize", isSignal: true, isRequired: false, transformFunction: null }, snapSize: { classPropertyName: "snapSize", publicName: "snapSize", isSignal: true, isRequired: false, transformFunction: null }, order: { classPropertyName: "order", publicName: "order", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sizeChange: "sizeChange", collapsedChange: "collapsedChange" }, host: { properties: { "attr.id": "_paneId()", "style.flex-basis": "_basis()", "style.min-width": "\"0\"", "style.min-height": "\"0\"", "style.order": "_index() * 2", "attr.data-split-pane-collapsed": "_collapsed()" }, classAttribute: "overflow-auto" }, ngImport: i0, template: `<ng-content />`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
85
|
+
}
|
|
86
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitPaneComponent, decorators: [{
|
|
87
|
+
type: Component,
|
|
88
|
+
args: [{
|
|
89
|
+
selector: 'tw-split-pane',
|
|
90
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
91
|
+
host: {
|
|
92
|
+
'[attr.id]': '_paneId()',
|
|
93
|
+
'[style.flex-basis]': '_basis()',
|
|
94
|
+
'[style.min-width]': '"0"',
|
|
95
|
+
'[style.min-height]': '"0"',
|
|
96
|
+
'[style.order]': '_index() * 2',
|
|
97
|
+
'[attr.data-split-pane-collapsed]': '_collapsed()',
|
|
98
|
+
class: 'overflow-auto',
|
|
99
|
+
},
|
|
100
|
+
template: `<ng-content />`,
|
|
101
|
+
}]
|
|
102
|
+
}], propDecorators: { defaultSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "defaultSize", required: false }] }], minSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "minSize", required: false }] }], maxSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "maxSize", required: false }] }], collapsible: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsible", required: false }] }], collapsedSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "collapsedSize", required: false }] }], snapSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "snapSize", required: false }] }], order: [{ type: i0.Input, args: [{ isSignal: true, alias: "order", required: false }] }], sizeChange: [{ type: i0.Output, args: ["sizeChange"] }], collapsedChange: [{ type: i0.Output, args: ["collapsedChange"] }] } });
|
|
103
|
+
|
|
104
|
+
const EPSILON = 0.01;
|
|
105
|
+
// ── Helpers ───────────────────────────────────────────────────────────────────
|
|
106
|
+
/** Clamps `value` into `[min, max]` without wrapping. */
|
|
107
|
+
function clampSize(value, min, max) {
|
|
108
|
+
return Math.min(Math.max(value, min), max);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Available space for pane content after subtracting gutters.
|
|
112
|
+
* For percent mode this is used only when we need a px reference (e.g., pixel
|
|
113
|
+
* unit sizing or overflow detection); percent sizes are always relative to this
|
|
114
|
+
* space but expressed as 0-100 sums.
|
|
115
|
+
*/
|
|
116
|
+
function availableSpace(containerPx, numPanes, gutterSize) {
|
|
117
|
+
return Math.max(0, containerPx - Math.max(0, numPanes - 1) * gutterSize);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* CSS flex-basis string for a pane.
|
|
121
|
+
*
|
|
122
|
+
* Percent mode: sizes represent percentages of the available space (container
|
|
123
|
+
* minus gutters). Since CSS `%` is relative to the flex container (which
|
|
124
|
+
* includes gutter space), we subtract each pane's proportional share of total
|
|
125
|
+
* gutter space via `calc()`. This requires no knowledge of the container's
|
|
126
|
+
* pixel size at render time.
|
|
127
|
+
*
|
|
128
|
+
* Pixel mode: a simple pixel value.
|
|
129
|
+
*/
|
|
130
|
+
function computeBasis(size, unit, numPanes, gutterSize) {
|
|
131
|
+
if (unit === 'pixel')
|
|
132
|
+
return `${size}px`;
|
|
133
|
+
const totalGutterPx = Math.max(0, numPanes - 1) * gutterSize;
|
|
134
|
+
const gutterShare = (size / 100) * totalGutterPx;
|
|
135
|
+
// Round to 4 decimal places so the CSS string is deterministic and readable.
|
|
136
|
+
const pct = +size.toFixed(4);
|
|
137
|
+
const gutter = +gutterShare.toFixed(4);
|
|
138
|
+
return `calc(${pct}% - ${gutter}px)`;
|
|
139
|
+
}
|
|
140
|
+
// ── Constraint resolver ───────────────────────────────────────────────────────
|
|
141
|
+
/**
|
|
142
|
+
* Resolve initial sizes from pane configs.
|
|
143
|
+
* Priority (§4.2): defaultSize → even distribution.
|
|
144
|
+
* Results are clamped against minSize/maxSize.
|
|
145
|
+
*/
|
|
146
|
+
function resolveInitialSizes(panes, unit, availablePx) {
|
|
147
|
+
const n = panes.length;
|
|
148
|
+
if (n === 0)
|
|
149
|
+
return [];
|
|
150
|
+
// Pixel mode with unknown container: use declared defaults directly.
|
|
151
|
+
// ResizeObserver will scale them once the real container size is known.
|
|
152
|
+
if (unit === 'pixel' && availablePx <= 0) {
|
|
153
|
+
return panes.map(p => clampSize(p.defaultSize ?? 0, p.minSize, p.maxSize));
|
|
154
|
+
}
|
|
155
|
+
const totalUnit = unit === 'percent' ? 100 : availablePx;
|
|
156
|
+
const declaredSum = panes.reduce((acc, p) => acc + (p.defaultSize ?? 0), 0);
|
|
157
|
+
const undeclaredCount = panes.filter(p => p.defaultSize === undefined).length;
|
|
158
|
+
const remaining = Math.max(0, totalUnit - declaredSum);
|
|
159
|
+
const undeclaredShare = undeclaredCount > 0 ? remaining / undeclaredCount : 0;
|
|
160
|
+
const raw = panes.map(p => p.defaultSize ?? undeclaredShare);
|
|
161
|
+
return redistributeWithConstraints(raw, panes, totalUnit);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Clamp sizes against per-pane min/max, redistributing slack proportionally
|
|
165
|
+
* among unclamped panes until the target total is satisfied (§4.2).
|
|
166
|
+
*
|
|
167
|
+
* Iterates up to `n + 1` times — sufficient for any acyclic constraint graph.
|
|
168
|
+
*/
|
|
169
|
+
function redistributeWithConstraints(sizes, panes, totalUnit) {
|
|
170
|
+
const n = sizes.length;
|
|
171
|
+
// Start with all values clamped to their feasible range.
|
|
172
|
+
const result = sizes.map((s, i) => clampSize(s, panes[i].minSize, panes[i].maxSize));
|
|
173
|
+
for (let iter = 0; iter <= n; iter++) {
|
|
174
|
+
const currentSum = result.reduce((a, b) => a + b, 0);
|
|
175
|
+
if (Math.abs(currentSum - totalUnit) < EPSILON)
|
|
176
|
+
break;
|
|
177
|
+
// Find panes that can flex in the direction needed to reach the target.
|
|
178
|
+
const flex = [];
|
|
179
|
+
if (currentSum < totalUnit) {
|
|
180
|
+
for (let i = 0; i < n; i++) {
|
|
181
|
+
if (result[i] < panes[i].maxSize - EPSILON)
|
|
182
|
+
flex.push(i);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
for (let i = 0; i < n; i++) {
|
|
187
|
+
if (result[i] > panes[i].minSize + EPSILON)
|
|
188
|
+
flex.push(i);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (flex.length === 0)
|
|
192
|
+
break; // constraints infeasible — best-effort result
|
|
193
|
+
const flexSet = new Set(flex);
|
|
194
|
+
const fixedSum = result.reduce((acc, s, i) => (flexSet.has(i) ? acc : acc + s), 0);
|
|
195
|
+
const flexTarget = totalUnit - fixedSum;
|
|
196
|
+
const flexCurrent = flex.reduce((acc, i) => acc + result[i], 0);
|
|
197
|
+
if (Math.abs(flexCurrent - flexTarget) < EPSILON)
|
|
198
|
+
break;
|
|
199
|
+
if (flexCurrent <= EPSILON) {
|
|
200
|
+
const share = flexTarget / flex.length;
|
|
201
|
+
for (const i of flex)
|
|
202
|
+
result[i] = clampSize(share, panes[i].minSize, panes[i].maxSize);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
const scale = flexTarget / flexCurrent;
|
|
206
|
+
for (const i of flex)
|
|
207
|
+
result[i] = clampSize(result[i] * scale, panes[i].minSize, panes[i].maxSize);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
// ── Container resize rescaling (§4.3) ─────────────────────────────────────────
|
|
213
|
+
/**
|
|
214
|
+
* Rescale sizes after a container resize event.
|
|
215
|
+
*
|
|
216
|
+
* - Percent mode: proportions (percentages) are preserved; re-clamp only.
|
|
217
|
+
* - Pixel mode: scale sizes proportionally to the new available space, then
|
|
218
|
+
* re-clamp.
|
|
219
|
+
*
|
|
220
|
+
* Returns the new sizes and a flag indicating whether any pane was clamped
|
|
221
|
+
* beyond its proportional share (used to decide whether to fire sizesChange).
|
|
222
|
+
*/
|
|
223
|
+
function rescaleForContainerResize(currentSizes, panes, unit, oldAvailablePx, newAvailablePx) {
|
|
224
|
+
const n = currentSizes.length;
|
|
225
|
+
if (n === 0)
|
|
226
|
+
return { sizes: [], clamped: false };
|
|
227
|
+
const totalUnit = unit === 'percent' ? 100 : newAvailablePx;
|
|
228
|
+
let proposed;
|
|
229
|
+
if (unit === 'percent') {
|
|
230
|
+
proposed = [...currentSizes];
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const ratio = oldAvailablePx > EPSILON ? newAvailablePx / oldAvailablePx : 1;
|
|
234
|
+
proposed = currentSizes.map(s => s * ratio);
|
|
235
|
+
}
|
|
236
|
+
const clamped = redistributeWithConstraints(proposed, panes, totalUnit);
|
|
237
|
+
const wasClamped = clamped.some((s, i) => Math.abs(s - proposed[i]) > EPSILON);
|
|
238
|
+
return { sizes: clamped, clamped: wasClamped };
|
|
239
|
+
}
|
|
240
|
+
// ── Pane add / remove (§4.4) ──────────────────────────────────────────────────
|
|
241
|
+
/**
|
|
242
|
+
* Compute sizes after a new pane is inserted at `newPaneIndex`.
|
|
243
|
+
* The new pane enters at its defaultSize (or an equal share if unset);
|
|
244
|
+
* existing panes shrink proportionally to accommodate it.
|
|
245
|
+
*/
|
|
246
|
+
function redistributeOnPaneAdded(currentSizes, newPaneIndex, newPaneConfig, allPaneConfigs, unit, availablePx) {
|
|
247
|
+
const n = allPaneConfigs.length;
|
|
248
|
+
const totalUnit = unit === 'percent' ? 100 : availablePx;
|
|
249
|
+
const newSize = clampSize(newPaneConfig.defaultSize ?? totalUnit / n, newPaneConfig.minSize, newPaneConfig.maxSize);
|
|
250
|
+
const existingTarget = Math.max(0, totalUnit - newSize);
|
|
251
|
+
const existingSum = currentSizes.reduce((a, b) => a + b, 0);
|
|
252
|
+
const scaled = existingSum > EPSILON
|
|
253
|
+
? currentSizes.map(s => (s / existingSum) * existingTarget)
|
|
254
|
+
: currentSizes.map(() => existingTarget / Math.max(1, currentSizes.length));
|
|
255
|
+
const result = [...scaled];
|
|
256
|
+
result.splice(newPaneIndex, 0, newSize);
|
|
257
|
+
return redistributeWithConstraints(result, allPaneConfigs, totalUnit);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Compute sizes after the pane at `removedIndex` is removed.
|
|
261
|
+
* Its space is redistributed proportionally to remaining panes.
|
|
262
|
+
*/
|
|
263
|
+
function redistributeOnPaneRemoved(currentSizes, removedIndex, remainingPaneConfigs, unit, availablePx) {
|
|
264
|
+
const remaining = currentSizes.filter((_, i) => i !== removedIndex);
|
|
265
|
+
if (remaining.length === 0)
|
|
266
|
+
return [];
|
|
267
|
+
const totalUnit = unit === 'percent' ? 100 : availablePx;
|
|
268
|
+
const remainingSum = remaining.reduce((a, b) => a + b, 0);
|
|
269
|
+
const scaled = remainingSum > EPSILON
|
|
270
|
+
? remaining.map(s => (s / remainingSum) * totalUnit)
|
|
271
|
+
: remaining.map(() => totalUnit / remaining.length);
|
|
272
|
+
return redistributeWithConstraints(scaled, remainingPaneConfigs, totalUnit);
|
|
273
|
+
}
|
|
274
|
+
// ── Overflow check (§4.2) ─────────────────────────────────────────────────────
|
|
275
|
+
/** Returns true when the sum of all minSizes exceeds the available space. */
|
|
276
|
+
function hasMinSizeOverflow(panes, unit, availablePx) {
|
|
277
|
+
const totalUnit = unit === 'percent' ? 100 : availablePx;
|
|
278
|
+
const minSum = panes.reduce((acc, p) => acc + p.minSize, 0);
|
|
279
|
+
return minSum > totalUnit + EPSILON;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
let nextSplitId = 0;
|
|
283
|
+
const splitVariants = tv({
|
|
284
|
+
slots: {
|
|
285
|
+
root: 'flex h-full w-full',
|
|
286
|
+
gutter: 'shrink-0 bg-border transition-colors duration-normal motion-reduce:transition-none hover:bg-border-strong focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-500',
|
|
287
|
+
},
|
|
288
|
+
variants: {
|
|
289
|
+
direction: {
|
|
290
|
+
horizontal: { root: 'flex-row', gutter: 'cursor-col-resize' },
|
|
291
|
+
vertical: { root: 'flex-col', gutter: 'cursor-row-resize' },
|
|
292
|
+
},
|
|
293
|
+
disabled: {
|
|
294
|
+
true: { gutter: 'opacity-50 pointer-events-none cursor-default' },
|
|
295
|
+
false: {},
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
defaultVariants: {
|
|
299
|
+
direction: 'horizontal',
|
|
300
|
+
disabled: false,
|
|
301
|
+
},
|
|
302
|
+
}, { twMerge: true });
|
|
303
|
+
/**
|
|
304
|
+
* Container component that lays out two or more panes along a single axis and
|
|
305
|
+
* lets the user resize them by dragging the gutters between them.
|
|
306
|
+
*
|
|
307
|
+
* @example
|
|
308
|
+
* ```html
|
|
309
|
+
* <tw-split direction="horizontal" [gutterSize]="6">
|
|
310
|
+
* <tw-split-pane [defaultSize]="30" [minSize]="15">…</tw-split-pane>
|
|
311
|
+
* <tw-split-pane [defaultSize]="70">…</tw-split-pane>
|
|
312
|
+
* </tw-split>
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
class SplitComponent {
|
|
316
|
+
/** Axis along which panes are laid out. `'horizontal'` = side-by-side; `'vertical'` = stacked. Defaults to `'horizontal'`. */
|
|
317
|
+
direction = input('horizontal', ...(ngDevMode ? [{ debugName: "direction" }] : /* istanbul ignore next */ []));
|
|
318
|
+
/** How sizes are expressed and reported. A single unit governs the whole container. Defaults to `'percent'`. */
|
|
319
|
+
unit = input('percent', ...(ngDevMode ? [{ debugName: "unit" }] : /* istanbul ignore next */ []));
|
|
320
|
+
/** Thickness of each gutter in pixels, perpendicular to the split axis. Defaults to `6`. */
|
|
321
|
+
gutterSize = input(6, ...(ngDevMode ? [{ debugName: "gutterSize" }] : /* istanbul ignore next */ []));
|
|
322
|
+
/** When true, all resize interactions are disabled. Gutters are not focusable and do not respond to input. Defaults to `false`. */
|
|
323
|
+
disabled = input(false, ...(ngDevMode ? [{ debugName: "disabled" }] : /* istanbul ignore next */ []));
|
|
324
|
+
/** How much to move the gutter per arrow-key press, in the container's declared unit. Defaults to `10`. */
|
|
325
|
+
keyboardStep = input(10, ...(ngDevMode ? [{ debugName: "keyboardStep" }] : /* istanbul ignore next */ []));
|
|
326
|
+
/** Step size for `PageUp` / `PageDown` key presses, in the container's declared unit. Defaults to `50`. */
|
|
327
|
+
keyboardStepLarge = input(50, ...(ngDevMode ? [{ debugName: "keyboardStepLarge" }] : /* istanbul ignore next */ []));
|
|
328
|
+
/**
|
|
329
|
+
* If non-null, pane sizes are persisted to `localStorage` under this key and restored on init.
|
|
330
|
+
* Defaults to `null` (persistence disabled).
|
|
331
|
+
*/
|
|
332
|
+
storageKey = input(null, ...(ngDevMode ? [{ debugName: "storageKey" }] : /* istanbul ignore next */ []));
|
|
333
|
+
/**
|
|
334
|
+
* When true, horizontal direction is visually reversed (RTL layout).
|
|
335
|
+
* Defaults to `null`, which means the value is inherited from the nearest ancestor `dir` attribute.
|
|
336
|
+
*/
|
|
337
|
+
rtl = input(null, ...(ngDevMode ? [{ debugName: "rtl" }] : /* istanbul ignore next */ []));
|
|
338
|
+
/**
|
|
339
|
+
* Fires after any committed resize with the ordered array of current pane sizes.
|
|
340
|
+
* Does **not** fire on every pointer move during drag — only on commit (release, keyboard step, programmatic call).
|
|
341
|
+
*/
|
|
342
|
+
sizesChange = output();
|
|
343
|
+
/** Fires on pointer/touch down or keyboard-initiated resize start. */
|
|
344
|
+
resizeStart = output();
|
|
345
|
+
/** Fires on pointer/touch up, blur, or keyboard resize commit. */
|
|
346
|
+
resizeEnd = output();
|
|
347
|
+
/** Fires when a pane collapses or expands via snap, keyboard, or programmatic API. */
|
|
348
|
+
collapseChange = output();
|
|
349
|
+
_elRef = inject(ElementRef);
|
|
350
|
+
_ngZone = inject(NgZone);
|
|
351
|
+
_destroyRef = inject(DestroyRef);
|
|
352
|
+
_directionality = inject(Directionality, { optional: true });
|
|
353
|
+
_isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
|
|
354
|
+
_panes = contentChildren(SplitPaneComponent, ...(ngDevMode ? [{ debugName: "_panes" }] : /* istanbul ignore next */ []));
|
|
355
|
+
/** Stable id used to namespace per-pane DOM ids inside this container. */
|
|
356
|
+
_componentId = `tw-split-${nextSplitId++}`;
|
|
357
|
+
/**
|
|
358
|
+
* @internal
|
|
359
|
+
* Container dimension in pixels along the split axis, updated by ResizeObserver.
|
|
360
|
+
* Starts at 0 until the first observation fires.
|
|
361
|
+
*/
|
|
362
|
+
_containerSizePx = signal(0, ...(ngDevMode ? [{ debugName: "_containerSizePx" }] : /* istanbul ignore next */ []));
|
|
363
|
+
/**
|
|
364
|
+
* @internal
|
|
365
|
+
* Current pane sizes in the container's declared unit.
|
|
366
|
+
* Percent mode: values sum to 100 (within epsilon).
|
|
367
|
+
* Pixel mode: values are absolute pixel sizes.
|
|
368
|
+
*/
|
|
369
|
+
_sizes = signal([], ...(ngDevMode ? [{ debugName: "_sizes" }] : /* istanbul ignore next */ []));
|
|
370
|
+
/** Tracks the live `dir` attribute resolved through CDK Directionality. */
|
|
371
|
+
_cdkDir = signal(this._directionality?.value ?? 'ltr', ...(ngDevMode ? [{ debugName: "_cdkDir" }] : /* istanbul ignore next */ []));
|
|
372
|
+
/** Resolves the effective RTL state: explicit `rtl` input overrides CDK `dir`. */
|
|
373
|
+
_isRtl = computed(() => this.rtl() ?? this._cdkDir() === 'rtl', ...(ngDevMode ? [{ debugName: "_isRtl" }] : /* istanbul ignore next */ []));
|
|
374
|
+
// Previous pane refs — plain field, not a signal, so the pane effect does
|
|
375
|
+
// not re-run just because we wrote this value.
|
|
376
|
+
_prevPaneRefs = null;
|
|
377
|
+
/** True until the first pane-list reconciliation has run; gates persistence hydration. */
|
|
378
|
+
_initialReconciled = false;
|
|
379
|
+
/** Resolved class strings from tailwind-variants. */
|
|
380
|
+
_classes = computed(() => splitVariants({
|
|
381
|
+
direction: this.direction(),
|
|
382
|
+
disabled: this.disabled(),
|
|
383
|
+
}), ...(ngDevMode ? [{ debugName: "_classes" }] : /* istanbul ignore next */ []));
|
|
384
|
+
_rootClass = computed(() => this._classes().root(), ...(ngDevMode ? [{ debugName: "_rootClass" }] : /* istanbul ignore next */ []));
|
|
385
|
+
_gutterClass = computed(() => this._classes().gutter(), ...(ngDevMode ? [{ debugName: "_gutterClass" }] : /* istanbul ignore next */ []));
|
|
386
|
+
/** Indices `0..panes.length - 2` representing the gutter between pane[i] and pane[i+1]. */
|
|
387
|
+
_gutterIndices = computed(() => {
|
|
388
|
+
const n = this._panes().length;
|
|
389
|
+
return n > 0 ? Array.from({ length: n - 1 }, (_, i) => i) : [];
|
|
390
|
+
}, ...(ngDevMode ? [{ debugName: "_gutterIndices" }] : /* istanbul ignore next */ []));
|
|
391
|
+
/** @internal — drag state for the active pointer. */
|
|
392
|
+
_drag = null;
|
|
393
|
+
constructor() {
|
|
394
|
+
// ResizeObserver — runs outside Angular zone to avoid CD on every resize.
|
|
395
|
+
// Guard against environments (e.g. jsdom in tests) where ResizeObserver
|
|
396
|
+
// is not available; those environments call _onContainerResize() directly.
|
|
397
|
+
afterNextRender(() => {
|
|
398
|
+
if (typeof ResizeObserver === 'undefined')
|
|
399
|
+
return;
|
|
400
|
+
const el = this._elRef.nativeElement;
|
|
401
|
+
const ro = new ResizeObserver(entries => {
|
|
402
|
+
const rect = entries[0]?.contentRect;
|
|
403
|
+
if (!rect)
|
|
404
|
+
return;
|
|
405
|
+
const sizePx = this.direction() === 'horizontal' ? rect.width : rect.height;
|
|
406
|
+
this._ngZone.run(() => this._onContainerResize(sizePx));
|
|
407
|
+
});
|
|
408
|
+
ro.observe(el);
|
|
409
|
+
this._destroyRef.onDestroy(() => ro.disconnect());
|
|
410
|
+
});
|
|
411
|
+
// Pane-list effect — re-runs whenever the ContentChildren list changes.
|
|
412
|
+
// Container size is read with untracked() so this effect does NOT re-run
|
|
413
|
+
// on container resize (the ResizeObserver handles that path independently).
|
|
414
|
+
effect(() => {
|
|
415
|
+
const panes = this._panes();
|
|
416
|
+
const containerPx = untracked(() => this._containerSizePx());
|
|
417
|
+
this._onPanesChange(panes, containerPx);
|
|
418
|
+
});
|
|
419
|
+
// Reflect CDK Directionality changes into the local signal so the computed
|
|
420
|
+
// _isRtl re-evaluates and keyboard / pointer handlers honour live `dir`
|
|
421
|
+
// attribute changes (e.g. when an ancestor flips the page direction).
|
|
422
|
+
if (this._directionality) {
|
|
423
|
+
const sub = this._directionality.change.subscribe(value => {
|
|
424
|
+
this._cdkDir.set(value);
|
|
425
|
+
});
|
|
426
|
+
this._destroyRef.onDestroy(() => sub.unsubscribe());
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
// ── Internal resize handler ─────────────────────────────────────────────────
|
|
430
|
+
/**
|
|
431
|
+
* @internal — exposed for testing; do not call from application code.
|
|
432
|
+
* Called by the ResizeObserver when the container's pixel size changes.
|
|
433
|
+
*/
|
|
434
|
+
_onContainerResize(newSizePx) {
|
|
435
|
+
const oldSizePx = this._containerSizePx();
|
|
436
|
+
if (Math.abs(newSizePx - oldSizePx) < 0.5)
|
|
437
|
+
return;
|
|
438
|
+
const panes = this._panes();
|
|
439
|
+
const unit = this.unit();
|
|
440
|
+
const gutterSize = this.gutterSize();
|
|
441
|
+
const n = panes.length;
|
|
442
|
+
const oldAvailable = availableSpace(oldSizePx, n, gutterSize);
|
|
443
|
+
const newAvailable = availableSpace(newSizePx, n, gutterSize);
|
|
444
|
+
const currentSizes = this._sizes();
|
|
445
|
+
const configs = panes.map(toPaneConfig);
|
|
446
|
+
const { sizes, clamped } = rescaleForContainerResize(currentSizes, configs, unit, oldAvailable, newAvailable);
|
|
447
|
+
this._containerSizePx.set(newSizePx);
|
|
448
|
+
this._sizes.set(sizes);
|
|
449
|
+
this._applyToPanes(panes, sizes, unit, n, gutterSize);
|
|
450
|
+
// Fire sizesChange only when clamping altered a proportional share (§4.3)
|
|
451
|
+
if (clamped) {
|
|
452
|
+
this._commit(sizes);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
// ── Pane-list reconciliation ────────────────────────────────────────────────
|
|
456
|
+
/**
|
|
457
|
+
* @internal — exposed for testing; do not call from application code.
|
|
458
|
+
* Called by the contentChildren effect when the pane list changes.
|
|
459
|
+
*/
|
|
460
|
+
_onPanesChange(panes, containerPx) {
|
|
461
|
+
const unit = this.unit();
|
|
462
|
+
const gutterSize = this.gutterSize();
|
|
463
|
+
const n = panes.length;
|
|
464
|
+
const available = availableSpace(containerPx, n, gutterSize);
|
|
465
|
+
// Assign a stable DOM id to each pane so aria-controls on the gutter can
|
|
466
|
+
// reference them. Idempotent — only the first assignment for a given pane
|
|
467
|
+
// sticks; later runs are no-ops.
|
|
468
|
+
for (let i = 0; i < panes.length; i++) {
|
|
469
|
+
panes[i]._ensureId(this._componentId, i);
|
|
470
|
+
}
|
|
471
|
+
if (this._prevPaneRefs === null) {
|
|
472
|
+
const restored = this._readPersistedSizes(panes);
|
|
473
|
+
if (restored) {
|
|
474
|
+
this._sizes.set(restored);
|
|
475
|
+
this._applyToPanes(panes, restored, unit, panes.length, gutterSize);
|
|
476
|
+
}
|
|
477
|
+
else {
|
|
478
|
+
this._initializeSizes(panes, unit, available, gutterSize);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
else {
|
|
482
|
+
this._reconcilePanes(panes, this._prevPaneRefs, unit, available, gutterSize);
|
|
483
|
+
}
|
|
484
|
+
this._prevPaneRefs = panes;
|
|
485
|
+
this._initialReconciled = true;
|
|
486
|
+
}
|
|
487
|
+
_initializeSizes(panes, unit, available, gutterSize) {
|
|
488
|
+
if (panes.length === 0) {
|
|
489
|
+
this._sizes.set([]);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
const configs = panes.map(toPaneConfig);
|
|
493
|
+
if (available > 0 && isDevMode() && hasMinSizeOverflow(configs, unit, available)) {
|
|
494
|
+
console.warn(`SplitComponent: sum of minSize values exceeds available space (${available}${unit === 'percent' ? '%' : 'px'}). Panes will overflow.`);
|
|
495
|
+
}
|
|
496
|
+
const sizes = resolveInitialSizes(configs, unit, available);
|
|
497
|
+
this._sizes.set(sizes);
|
|
498
|
+
this._applyToPanes(panes, sizes, unit, panes.length, gutterSize);
|
|
499
|
+
}
|
|
500
|
+
_reconcilePanes(newPanes, prevPanes, unit, available, gutterSize) {
|
|
501
|
+
// `_reconcilePanes` runs from inside the contentChildren effect; reading
|
|
502
|
+
// `_sizes` tracked + writing `_sizes.set(newArray)` below would re-fire the
|
|
503
|
+
// effect on every CD tick (new array reference fails Object.is equality).
|
|
504
|
+
const currentSizes = untracked(() => this._sizes());
|
|
505
|
+
let sizes = [...currentSizes];
|
|
506
|
+
const prevSet = new Set(prevPanes);
|
|
507
|
+
const newSet = new Set(newPanes);
|
|
508
|
+
const added = newPanes.filter(p => !prevSet.has(p));
|
|
509
|
+
const removed = prevPanes.filter(p => !newSet.has(p));
|
|
510
|
+
// Process removals first (working against the prev order)
|
|
511
|
+
let workingPanes = [...prevPanes];
|
|
512
|
+
for (const removedPane of removed) {
|
|
513
|
+
const idx = workingPanes.indexOf(removedPane);
|
|
514
|
+
if (idx === -1)
|
|
515
|
+
continue;
|
|
516
|
+
workingPanes = workingPanes.filter((_, i) => i !== idx);
|
|
517
|
+
sizes = redistributeOnPaneRemoved(sizes, idx, workingPanes.map(toPaneConfig), unit, available);
|
|
518
|
+
}
|
|
519
|
+
// Process additions — `sizes` already represents workingPanes (without the
|
|
520
|
+
// new pane). redistributeOnPaneAdded inserts the new size at newIdx internally.
|
|
521
|
+
for (const addedPane of added) {
|
|
522
|
+
const newIdx = newPanes.indexOf(addedPane);
|
|
523
|
+
workingPanes = [...workingPanes];
|
|
524
|
+
workingPanes.splice(newIdx, 0, addedPane);
|
|
525
|
+
sizes = redistributeOnPaneAdded(sizes, newIdx, toPaneConfig(addedPane), workingPanes.map(toPaneConfig), unit, available);
|
|
526
|
+
}
|
|
527
|
+
this._sizes.set(sizes);
|
|
528
|
+
this._applyToPanes(newPanes, sizes, unit, newPanes.length, gutterSize);
|
|
529
|
+
}
|
|
530
|
+
// ── Apply sizes to pane DOM ─────────────────────────────────────────────────
|
|
531
|
+
_applyToPanes(panes, sizes, unit, numPanes, gutterSize) {
|
|
532
|
+
for (let i = 0; i < panes.length; i++) {
|
|
533
|
+
const size = sizes[i] ?? 0;
|
|
534
|
+
// Read the previous size without subscribing — `_applyToPanes` runs from
|
|
535
|
+
// an effect (initial reconciliation) as well as direct calls, and a
|
|
536
|
+
// tracked read combined with the write below would loop the effect
|
|
537
|
+
// indefinitely.
|
|
538
|
+
const prev = untracked(() => panes[i]._size());
|
|
539
|
+
panes[i]._index.set(i);
|
|
540
|
+
panes[i]._basis.set(computeBasis(size, unit, numPanes, gutterSize));
|
|
541
|
+
panes[i]._size.set(size);
|
|
542
|
+
if (Math.abs(prev - size) > 1e-6) {
|
|
543
|
+
panes[i].sizeChange.emit(size);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
// ── Public API ──────────────────────────────────────────────────────────────
|
|
548
|
+
/**
|
|
549
|
+
* Programmatically set all pane sizes.
|
|
550
|
+
* The array length must equal the current pane count and sizes must be
|
|
551
|
+
* compatible with the declared unit. Throws on mismatch.
|
|
552
|
+
*/
|
|
553
|
+
setSizes(sizes) {
|
|
554
|
+
const panes = this._panes();
|
|
555
|
+
if (sizes.length !== panes.length) {
|
|
556
|
+
throw new Error(`SplitComponent.setSizes: expected ${panes.length} sizes, got ${sizes.length}`);
|
|
557
|
+
}
|
|
558
|
+
const unit = this.unit();
|
|
559
|
+
const containerPx = this._containerSizePx();
|
|
560
|
+
const gutterSize = this.gutterSize();
|
|
561
|
+
const n = panes.length;
|
|
562
|
+
const available = availableSpace(containerPx, n, gutterSize);
|
|
563
|
+
const totalUnit = unit === 'percent' ? 100 : available;
|
|
564
|
+
const configs = panes.map(toPaneConfig);
|
|
565
|
+
const clamped = redistributeWithConstraints(sizes, configs, totalUnit);
|
|
566
|
+
this._sizes.set(clamped);
|
|
567
|
+
this._applyToPanes(panes, clamped, unit, n, gutterSize);
|
|
568
|
+
this._commit(clamped);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Collapse the pane at `paneIndex` to its `collapsedSize`.
|
|
572
|
+
* The pane must have `collapsible = true`. Throws if the index is out of range.
|
|
573
|
+
*/
|
|
574
|
+
collapse(paneIndex) {
|
|
575
|
+
const panes = this._panes();
|
|
576
|
+
if (paneIndex < 0 || paneIndex >= panes.length) {
|
|
577
|
+
throw new Error(`SplitComponent.collapse: paneIndex ${paneIndex} out of range`);
|
|
578
|
+
}
|
|
579
|
+
const pane = panes[paneIndex];
|
|
580
|
+
if (!pane.collapsible()) {
|
|
581
|
+
throw new Error(`SplitComponent.collapse: pane ${paneIndex} is not marked collapsible`);
|
|
582
|
+
}
|
|
583
|
+
if (pane._collapsed())
|
|
584
|
+
return;
|
|
585
|
+
this._setCollapsed(panes, paneIndex, true, 'programmatic');
|
|
586
|
+
}
|
|
587
|
+
/**
|
|
588
|
+
* Restore the pane at `paneIndex` to its pre-collapse size, or its `defaultSize` if none is recorded.
|
|
589
|
+
* Throws if the index is out of range.
|
|
590
|
+
*/
|
|
591
|
+
expand(paneIndex) {
|
|
592
|
+
const panes = this._panes();
|
|
593
|
+
if (paneIndex < 0 || paneIndex >= panes.length) {
|
|
594
|
+
throw new Error(`SplitComponent.expand: paneIndex ${paneIndex} out of range`);
|
|
595
|
+
}
|
|
596
|
+
const pane = panes[paneIndex];
|
|
597
|
+
if (!pane._collapsed())
|
|
598
|
+
return;
|
|
599
|
+
this._setCollapsed(panes, paneIndex, false, 'programmatic');
|
|
600
|
+
}
|
|
601
|
+
/**
|
|
602
|
+
* Restore all panes to their declared `defaultSize` values.
|
|
603
|
+
* Clears any persisted sizes if `storageKey` is set.
|
|
604
|
+
*/
|
|
605
|
+
reset() {
|
|
606
|
+
const panes = this._panes();
|
|
607
|
+
const unit = this.unit();
|
|
608
|
+
const containerPx = this._containerSizePx();
|
|
609
|
+
const gutterSize = this.gutterSize();
|
|
610
|
+
const n = panes.length;
|
|
611
|
+
const available = availableSpace(containerPx, n, gutterSize);
|
|
612
|
+
for (const pane of panes) {
|
|
613
|
+
pane._collapsed.set(false);
|
|
614
|
+
pane._preCollapseSize.set(null);
|
|
615
|
+
}
|
|
616
|
+
this._initializeSizes(panes, unit, available, gutterSize);
|
|
617
|
+
if (panes.length > 0) {
|
|
618
|
+
this.sizesChange.emit([...this._sizes()]);
|
|
619
|
+
}
|
|
620
|
+
// Clear persisted sizes after emit so the cleared state is the resting
|
|
621
|
+
// state at the end of reset(). New commits will repopulate the entry.
|
|
622
|
+
this._clearPersistedSizes();
|
|
623
|
+
}
|
|
624
|
+
// ── ARIA / pointer / keyboard ───────────────────────────────────────────────
|
|
625
|
+
/** @internal — `aria-valuenow` for the gutter between pane i and i+1. Reports the LEFT pane size as a percent. */
|
|
626
|
+
_ariaValueNow(gutterIndex) {
|
|
627
|
+
const sizes = this._sizes();
|
|
628
|
+
const size = sizes[gutterIndex];
|
|
629
|
+
if (size === undefined)
|
|
630
|
+
return null;
|
|
631
|
+
if (this.unit() === 'percent')
|
|
632
|
+
return Math.round(size);
|
|
633
|
+
const available = availableSpace(this._containerSizePx(), this._panes().length, this.gutterSize());
|
|
634
|
+
if (available <= 0)
|
|
635
|
+
return null;
|
|
636
|
+
return Math.round((size / available) * 100);
|
|
637
|
+
}
|
|
638
|
+
/** @internal — `aria-controls` references the two adjacent panes' DOM ids. */
|
|
639
|
+
_ariaControls(gutterIndex) {
|
|
640
|
+
const panes = this._panes();
|
|
641
|
+
const a = panes[gutterIndex]?._paneId();
|
|
642
|
+
const b = panes[gutterIndex + 1]?._paneId();
|
|
643
|
+
if (!a || !b)
|
|
644
|
+
return null;
|
|
645
|
+
return `${a} ${b}`;
|
|
646
|
+
}
|
|
647
|
+
/** @internal — accessible name fallback for the separator handle. */
|
|
648
|
+
_ariaLabel(gutterIndex) {
|
|
649
|
+
return this.direction() === 'horizontal'
|
|
650
|
+
? `Resize column ${gutterIndex + 1}`
|
|
651
|
+
: `Resize row ${gutterIndex + 1}`;
|
|
652
|
+
}
|
|
653
|
+
/** @internal */
|
|
654
|
+
_onGutterPointerDown(event, gutterIndex) {
|
|
655
|
+
if (this.disabled() || event.button !== 0)
|
|
656
|
+
return;
|
|
657
|
+
const target = event.target;
|
|
658
|
+
target.setPointerCapture(event.pointerId);
|
|
659
|
+
const startClient = this.direction() === 'horizontal' ? event.clientX : event.clientY;
|
|
660
|
+
const startSizes = [...this._sizes()];
|
|
661
|
+
const moveHandler = (e) => this._onGutterPointerMove(e);
|
|
662
|
+
const upHandler = (e) => this._onGutterPointerUp(e);
|
|
663
|
+
target.addEventListener('pointermove', moveHandler);
|
|
664
|
+
target.addEventListener('pointerup', upHandler);
|
|
665
|
+
target.addEventListener('pointercancel', upHandler);
|
|
666
|
+
this._drag = {
|
|
667
|
+
pointerId: event.pointerId,
|
|
668
|
+
gutterIndex,
|
|
669
|
+
startClient,
|
|
670
|
+
startSizes,
|
|
671
|
+
target,
|
|
672
|
+
moveHandler,
|
|
673
|
+
upHandler,
|
|
674
|
+
};
|
|
675
|
+
this._setNoSelect(true);
|
|
676
|
+
this.resizeStart.emit({
|
|
677
|
+
sizes: [...startSizes],
|
|
678
|
+
unit: this.unit(),
|
|
679
|
+
originPaneIndex: gutterIndex,
|
|
680
|
+
cause: event.pointerType === 'touch' ? 'touch' : 'pointer',
|
|
681
|
+
});
|
|
682
|
+
event.preventDefault();
|
|
683
|
+
}
|
|
684
|
+
_onGutterPointerMove(event) {
|
|
685
|
+
const drag = this._drag;
|
|
686
|
+
if (!drag)
|
|
687
|
+
return;
|
|
688
|
+
const horizontal = this.direction() === 'horizontal';
|
|
689
|
+
const current = horizontal ? event.clientX : event.clientY;
|
|
690
|
+
let deltaPx = current - drag.startClient;
|
|
691
|
+
if (deltaPx === 0)
|
|
692
|
+
return;
|
|
693
|
+
// In RTL horizontal mode the visual delta is inverted from the pane order.
|
|
694
|
+
if (horizontal && this._isRtl())
|
|
695
|
+
deltaPx = -deltaPx;
|
|
696
|
+
const panes = this._panes();
|
|
697
|
+
const unit = this.unit();
|
|
698
|
+
const containerPx = this._containerSizePx();
|
|
699
|
+
const available = availableSpace(containerPx, panes.length, this.gutterSize());
|
|
700
|
+
if (available <= 0)
|
|
701
|
+
return;
|
|
702
|
+
const deltaUnit = unit === 'percent' ? (deltaPx / available) * 100 : deltaPx;
|
|
703
|
+
// Apply delta to gutterIndex (grows) and gutterIndex+1 (shrinks).
|
|
704
|
+
const next = [...drag.startSizes];
|
|
705
|
+
next[drag.gutterIndex] = drag.startSizes[drag.gutterIndex] + deltaUnit;
|
|
706
|
+
next[drag.gutterIndex + 1] = drag.startSizes[drag.gutterIndex + 1] - deltaUnit;
|
|
707
|
+
const totalUnit = unit === 'percent' ? 100 : available;
|
|
708
|
+
const configs = panes.map(toPaneConfig);
|
|
709
|
+
const clamped = redistributeWithConstraints(next, configs, totalUnit);
|
|
710
|
+
this._sizes.set(clamped);
|
|
711
|
+
this._applyToPanes(panes, clamped, unit, panes.length, this.gutterSize());
|
|
712
|
+
}
|
|
713
|
+
_onGutterPointerUp(event) {
|
|
714
|
+
const drag = this._drag;
|
|
715
|
+
if (!drag)
|
|
716
|
+
return;
|
|
717
|
+
const target = drag.target;
|
|
718
|
+
try {
|
|
719
|
+
target.releasePointerCapture(drag.pointerId);
|
|
720
|
+
}
|
|
721
|
+
catch {
|
|
722
|
+
/* pointer already released */
|
|
723
|
+
}
|
|
724
|
+
target.removeEventListener('pointermove', drag.moveHandler);
|
|
725
|
+
target.removeEventListener('pointerup', drag.upHandler);
|
|
726
|
+
target.removeEventListener('pointercancel', drag.upHandler);
|
|
727
|
+
this._drag = null;
|
|
728
|
+
this._setNoSelect(false);
|
|
729
|
+
const finalSizes = this._sizes();
|
|
730
|
+
// Snap-collapse threshold check on the two panes adjacent to the drag.
|
|
731
|
+
this._maybeSnap(drag.gutterIndex);
|
|
732
|
+
this._commit(this._sizes());
|
|
733
|
+
this.resizeEnd.emit({
|
|
734
|
+
sizes: [...finalSizes],
|
|
735
|
+
unit: this.unit(),
|
|
736
|
+
originPaneIndex: drag.gutterIndex,
|
|
737
|
+
cause: event.pointerType === 'touch' ? 'touch' : 'pointer',
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
/** @internal */
|
|
741
|
+
_onGutterKeydown(event, gutterIndex) {
|
|
742
|
+
if (this.disabled())
|
|
743
|
+
return;
|
|
744
|
+
const horizontal = this.direction() === 'horizontal';
|
|
745
|
+
const step = this.keyboardStep();
|
|
746
|
+
const stepLarge = this.keyboardStepLarge();
|
|
747
|
+
const rtl = horizontal && this._isRtl();
|
|
748
|
+
const unit = this.unit();
|
|
749
|
+
const containerPx = this._containerSizePx();
|
|
750
|
+
const available = availableSpace(containerPx, this._panes().length, this.gutterSize());
|
|
751
|
+
const totalUnit = unit === 'percent' ? 100 : available;
|
|
752
|
+
// Use the container's full extent for Home/End so the delta saturates the
|
|
753
|
+
// left pane against its bounds without resorting to Infinity (which would
|
|
754
|
+
// produce NaN once multiplied by the redistribution scaling factor).
|
|
755
|
+
const saturate = totalUnit > 0 ? totalUnit : 1e9;
|
|
756
|
+
let delta = null;
|
|
757
|
+
const cause = 'keyboard';
|
|
758
|
+
let restore = false;
|
|
759
|
+
switch (event.key) {
|
|
760
|
+
case 'ArrowLeft':
|
|
761
|
+
if (horizontal)
|
|
762
|
+
delta = rtl ? step : -step;
|
|
763
|
+
break;
|
|
764
|
+
case 'ArrowRight':
|
|
765
|
+
if (horizontal)
|
|
766
|
+
delta = rtl ? -step : step;
|
|
767
|
+
break;
|
|
768
|
+
case 'ArrowUp':
|
|
769
|
+
if (!horizontal)
|
|
770
|
+
delta = -step;
|
|
771
|
+
break;
|
|
772
|
+
case 'ArrowDown':
|
|
773
|
+
if (!horizontal)
|
|
774
|
+
delta = step;
|
|
775
|
+
break;
|
|
776
|
+
case 'PageUp':
|
|
777
|
+
delta = -stepLarge;
|
|
778
|
+
break;
|
|
779
|
+
case 'PageDown':
|
|
780
|
+
delta = stepLarge;
|
|
781
|
+
break;
|
|
782
|
+
case 'Home':
|
|
783
|
+
delta = -saturate;
|
|
784
|
+
break;
|
|
785
|
+
case 'End':
|
|
786
|
+
delta = saturate;
|
|
787
|
+
break;
|
|
788
|
+
case 'Enter':
|
|
789
|
+
case ' ':
|
|
790
|
+
restore = true;
|
|
791
|
+
break;
|
|
792
|
+
default:
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
if (delta === null && !restore)
|
|
796
|
+
return;
|
|
797
|
+
event.preventDefault();
|
|
798
|
+
const panes = this._panes();
|
|
799
|
+
const startSizes = [...this._sizes()];
|
|
800
|
+
this.resizeStart.emit({
|
|
801
|
+
sizes: [...startSizes],
|
|
802
|
+
unit,
|
|
803
|
+
originPaneIndex: gutterIndex,
|
|
804
|
+
cause,
|
|
805
|
+
});
|
|
806
|
+
if (restore) {
|
|
807
|
+
// Enter / Space toggles collapse on the LEFT pane of the focused gutter
|
|
808
|
+
// when collapsible; otherwise falls back to reset() for keyboard-only recovery.
|
|
809
|
+
const leftPane = panes[gutterIndex];
|
|
810
|
+
if (leftPane?.collapsible()) {
|
|
811
|
+
this._setCollapsed(panes, gutterIndex, !leftPane._collapsed(), 'keyboard');
|
|
812
|
+
this.resizeEnd.emit({
|
|
813
|
+
sizes: [...this._sizes()],
|
|
814
|
+
unit,
|
|
815
|
+
originPaneIndex: gutterIndex,
|
|
816
|
+
cause,
|
|
817
|
+
});
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
820
|
+
this.reset();
|
|
821
|
+
this.resizeEnd.emit({
|
|
822
|
+
sizes: [...this._sizes()],
|
|
823
|
+
unit,
|
|
824
|
+
originPaneIndex: gutterIndex,
|
|
825
|
+
cause,
|
|
826
|
+
});
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
const next = [...startSizes];
|
|
830
|
+
next[gutterIndex] = startSizes[gutterIndex] + delta;
|
|
831
|
+
next[gutterIndex + 1] = startSizes[gutterIndex + 1] - delta;
|
|
832
|
+
const configs = panes.map(toPaneConfig);
|
|
833
|
+
const clamped = redistributeWithConstraints(next, configs, totalUnit);
|
|
834
|
+
this._sizes.set(clamped);
|
|
835
|
+
this._applyToPanes(panes, clamped, unit, panes.length, this.gutterSize());
|
|
836
|
+
this._commit(clamped);
|
|
837
|
+
this.resizeEnd.emit({
|
|
838
|
+
sizes: [...clamped],
|
|
839
|
+
unit,
|
|
840
|
+
originPaneIndex: gutterIndex,
|
|
841
|
+
cause,
|
|
842
|
+
});
|
|
843
|
+
}
|
|
844
|
+
// ── Collapse helpers ────────────────────────────────────────────────────────
|
|
845
|
+
_setCollapsed(panes, paneIndex, collapsed, cause) {
|
|
846
|
+
const pane = panes[paneIndex];
|
|
847
|
+
const unit = this.unit();
|
|
848
|
+
const containerPx = this._containerSizePx();
|
|
849
|
+
const gutterSize = this.gutterSize();
|
|
850
|
+
const available = availableSpace(containerPx, panes.length, gutterSize);
|
|
851
|
+
const totalUnit = unit === 'percent' ? 100 : available;
|
|
852
|
+
const current = [...this._sizes()];
|
|
853
|
+
// Pick the neighbour that absorbs the size delta: prefer the right pane,
|
|
854
|
+
// fall back to the left pane when collapsing the last pane in the row.
|
|
855
|
+
const partner = paneIndex < panes.length - 1 ? paneIndex + 1 : paneIndex - 1;
|
|
856
|
+
const target = [...current];
|
|
857
|
+
const configs = panes.map(toPaneConfig);
|
|
858
|
+
if (collapsed) {
|
|
859
|
+
pane._preCollapseSize.set(current[paneIndex] ?? null);
|
|
860
|
+
const collapsedSize = pane.collapsedSize();
|
|
861
|
+
const delta = current[paneIndex] - collapsedSize;
|
|
862
|
+
target[paneIndex] = collapsedSize;
|
|
863
|
+
if (partner >= 0)
|
|
864
|
+
target[partner] = current[partner] + delta;
|
|
865
|
+
// Lock the collapsed pane to exactly `collapsedSize` during redistribution
|
|
866
|
+
// so slack does not flow back into it.
|
|
867
|
+
configs[paneIndex] = {
|
|
868
|
+
defaultSize: configs[paneIndex].defaultSize,
|
|
869
|
+
minSize: collapsedSize,
|
|
870
|
+
maxSize: collapsedSize,
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
else {
|
|
874
|
+
const restored = pane._preCollapseSize() ?? pane.defaultSize() ?? totalUnit / Math.max(1, panes.length);
|
|
875
|
+
const delta = restored - current[paneIndex];
|
|
876
|
+
target[paneIndex] = restored;
|
|
877
|
+
if (partner >= 0)
|
|
878
|
+
target[partner] = current[partner] - delta;
|
|
879
|
+
pane._preCollapseSize.set(null);
|
|
880
|
+
}
|
|
881
|
+
const next = redistributeWithConstraints(target, configs, totalUnit);
|
|
882
|
+
pane._collapsed.set(collapsed);
|
|
883
|
+
pane.collapsedChange.emit(collapsed);
|
|
884
|
+
this._sizes.set(next);
|
|
885
|
+
this._applyToPanes(panes, next, unit, panes.length, gutterSize);
|
|
886
|
+
this._commit(next);
|
|
887
|
+
this.collapseChange.emit({ paneIndex, collapsed, cause });
|
|
888
|
+
}
|
|
889
|
+
_maybeSnap(gutterIndex) {
|
|
890
|
+
const panes = this._panes();
|
|
891
|
+
const sizes = this._sizes();
|
|
892
|
+
for (const i of [gutterIndex, gutterIndex + 1]) {
|
|
893
|
+
const pane = panes[i];
|
|
894
|
+
if (!pane?.collapsible())
|
|
895
|
+
continue;
|
|
896
|
+
const snap = pane.snapSize();
|
|
897
|
+
if (snap <= 0)
|
|
898
|
+
continue;
|
|
899
|
+
const size = sizes[i] ?? 0;
|
|
900
|
+
const collapsed = pane._collapsed();
|
|
901
|
+
const collapsedSize = pane.collapsedSize();
|
|
902
|
+
const distance = Math.abs(size - collapsedSize);
|
|
903
|
+
if (!collapsed && distance <= snap && size > collapsedSize) {
|
|
904
|
+
this._setCollapsed(panes, i, true, 'snap');
|
|
905
|
+
}
|
|
906
|
+
else if (collapsed && size > collapsedSize + snap) {
|
|
907
|
+
this._setCollapsed(panes, i, false, 'snap');
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
// ── Persistence ─────────────────────────────────────────────────────────────
|
|
912
|
+
_readPersistedSizes(panes) {
|
|
913
|
+
const key = this.storageKey();
|
|
914
|
+
if (!key || !this._isBrowser || typeof localStorage === 'undefined')
|
|
915
|
+
return null;
|
|
916
|
+
try {
|
|
917
|
+
const raw = localStorage.getItem(key);
|
|
918
|
+
if (!raw)
|
|
919
|
+
return null;
|
|
920
|
+
const parsed = JSON.parse(raw);
|
|
921
|
+
if (!Array.isArray(parsed) || parsed.length !== panes.length)
|
|
922
|
+
return null;
|
|
923
|
+
if (!parsed.every((v) => typeof v === 'number' && Number.isFinite(v)))
|
|
924
|
+
return null;
|
|
925
|
+
const unit = this.unit();
|
|
926
|
+
const configs = panes.map(toPaneConfig);
|
|
927
|
+
// `_containerSizePx` is read untracked: this method runs inside the
|
|
928
|
+
// contentChildren effect, which already handles container-size changes
|
|
929
|
+
// through the separate ResizeObserver path.
|
|
930
|
+
const containerPx = untracked(() => this._containerSizePx());
|
|
931
|
+
const available = availableSpace(containerPx, panes.length, this.gutterSize());
|
|
932
|
+
const totalUnit = unit === 'percent' ? 100 : available;
|
|
933
|
+
// Re-clamp persisted sizes against current constraints. If totalUnit is 0
|
|
934
|
+
// (pixel mode pre-measurement), fall back to raw values for later rescaling.
|
|
935
|
+
if (totalUnit <= 0)
|
|
936
|
+
return parsed;
|
|
937
|
+
return redistributeWithConstraints(parsed, configs, totalUnit);
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
return null;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
_writePersistedSizes(sizes) {
|
|
944
|
+
const key = this.storageKey();
|
|
945
|
+
if (!key || !this._isBrowser || typeof localStorage === 'undefined')
|
|
946
|
+
return;
|
|
947
|
+
if (!this._initialReconciled)
|
|
948
|
+
return;
|
|
949
|
+
try {
|
|
950
|
+
localStorage.setItem(key, JSON.stringify(sizes));
|
|
951
|
+
}
|
|
952
|
+
catch {
|
|
953
|
+
/* quota / private-browsing — silently skip persistence */
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
_clearPersistedSizes() {
|
|
957
|
+
const key = this.storageKey();
|
|
958
|
+
if (!key || !this._isBrowser || typeof localStorage === 'undefined')
|
|
959
|
+
return;
|
|
960
|
+
try {
|
|
961
|
+
localStorage.removeItem(key);
|
|
962
|
+
}
|
|
963
|
+
catch {
|
|
964
|
+
/* swallow */
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
// ── Commit helper ──────────────────────────────────────────────────────────
|
|
968
|
+
_commit(sizes) {
|
|
969
|
+
this.sizesChange.emit([...sizes]);
|
|
970
|
+
this._writePersistedSizes(sizes);
|
|
971
|
+
}
|
|
972
|
+
// ── User-select suppression during drag ────────────────────────────────────
|
|
973
|
+
_setNoSelect(active) {
|
|
974
|
+
if (!this._isBrowser || typeof document === 'undefined')
|
|
975
|
+
return;
|
|
976
|
+
document.body.classList.toggle('tw-split-no-select', active);
|
|
977
|
+
}
|
|
978
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
979
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: SplitComponent, isStandalone: true, selector: "tw-split", inputs: { direction: { classPropertyName: "direction", publicName: "direction", isSignal: true, isRequired: false, transformFunction: null }, unit: { classPropertyName: "unit", publicName: "unit", isSignal: true, isRequired: false, transformFunction: null }, gutterSize: { classPropertyName: "gutterSize", publicName: "gutterSize", isSignal: true, isRequired: false, transformFunction: null }, disabled: { classPropertyName: "disabled", publicName: "disabled", isSignal: true, isRequired: false, transformFunction: null }, keyboardStep: { classPropertyName: "keyboardStep", publicName: "keyboardStep", isSignal: true, isRequired: false, transformFunction: null }, keyboardStepLarge: { classPropertyName: "keyboardStepLarge", publicName: "keyboardStepLarge", isSignal: true, isRequired: false, transformFunction: null }, storageKey: { classPropertyName: "storageKey", publicName: "storageKey", isSignal: true, isRequired: false, transformFunction: null }, rtl: { classPropertyName: "rtl", publicName: "rtl", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { sizesChange: "sizesChange", resizeStart: "resizeStart", resizeEnd: "resizeEnd", collapseChange: "collapseChange" }, host: { properties: { "class": "_rootClass()", "attr.data-split-direction": "direction()" } }, queries: [{ propertyName: "_panes", predicate: SplitPaneComponent, isSignal: true }], ngImport: i0, template: `
|
|
980
|
+
<ng-content />
|
|
981
|
+
@for (i of _gutterIndices(); track i) {
|
|
982
|
+
<div
|
|
983
|
+
role="separator"
|
|
984
|
+
[attr.aria-orientation]="direction()"
|
|
985
|
+
[attr.aria-valuemin]="0"
|
|
986
|
+
[attr.aria-valuemax]="100"
|
|
987
|
+
[attr.aria-valuenow]="_ariaValueNow(i)"
|
|
988
|
+
[attr.aria-controls]="_ariaControls(i)"
|
|
989
|
+
[attr.aria-label]="_ariaLabel(i)"
|
|
990
|
+
[attr.aria-disabled]="disabled() || null"
|
|
991
|
+
[attr.tabindex]="disabled() ? -1 : 0"
|
|
992
|
+
[class]="_gutterClass()"
|
|
993
|
+
[style.order]="i * 2 + 1"
|
|
994
|
+
[style.flex-basis.px]="gutterSize()"
|
|
995
|
+
[style.min-width.px]="direction() === 'horizontal' ? gutterSize() : 0"
|
|
996
|
+
[style.min-height.px]="direction() === 'vertical' ? gutterSize() : 0"
|
|
997
|
+
(pointerdown)="_onGutterPointerDown($event, i)"
|
|
998
|
+
(keydown)="_onGutterKeydown($event, i)"
|
|
999
|
+
></div>
|
|
1000
|
+
}
|
|
1001
|
+
`, isInline: true, changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1002
|
+
}
|
|
1003
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitComponent, decorators: [{
|
|
1004
|
+
type: Component,
|
|
1005
|
+
args: [{
|
|
1006
|
+
selector: 'tw-split',
|
|
1007
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
1008
|
+
host: {
|
|
1009
|
+
'[class]': '_rootClass()',
|
|
1010
|
+
'[attr.data-split-direction]': 'direction()',
|
|
1011
|
+
},
|
|
1012
|
+
template: `
|
|
1013
|
+
<ng-content />
|
|
1014
|
+
@for (i of _gutterIndices(); track i) {
|
|
1015
|
+
<div
|
|
1016
|
+
role="separator"
|
|
1017
|
+
[attr.aria-orientation]="direction()"
|
|
1018
|
+
[attr.aria-valuemin]="0"
|
|
1019
|
+
[attr.aria-valuemax]="100"
|
|
1020
|
+
[attr.aria-valuenow]="_ariaValueNow(i)"
|
|
1021
|
+
[attr.aria-controls]="_ariaControls(i)"
|
|
1022
|
+
[attr.aria-label]="_ariaLabel(i)"
|
|
1023
|
+
[attr.aria-disabled]="disabled() || null"
|
|
1024
|
+
[attr.tabindex]="disabled() ? -1 : 0"
|
|
1025
|
+
[class]="_gutterClass()"
|
|
1026
|
+
[style.order]="i * 2 + 1"
|
|
1027
|
+
[style.flex-basis.px]="gutterSize()"
|
|
1028
|
+
[style.min-width.px]="direction() === 'horizontal' ? gutterSize() : 0"
|
|
1029
|
+
[style.min-height.px]="direction() === 'vertical' ? gutterSize() : 0"
|
|
1030
|
+
(pointerdown)="_onGutterPointerDown($event, i)"
|
|
1031
|
+
(keydown)="_onGutterKeydown($event, i)"
|
|
1032
|
+
></div>
|
|
1033
|
+
}
|
|
1034
|
+
`,
|
|
1035
|
+
}]
|
|
1036
|
+
}], ctorParameters: () => [], propDecorators: { direction: [{ type: i0.Input, args: [{ isSignal: true, alias: "direction", required: false }] }], unit: [{ type: i0.Input, args: [{ isSignal: true, alias: "unit", required: false }] }], gutterSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "gutterSize", required: false }] }], disabled: [{ type: i0.Input, args: [{ isSignal: true, alias: "disabled", required: false }] }], keyboardStep: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStep", required: false }] }], keyboardStepLarge: [{ type: i0.Input, args: [{ isSignal: true, alias: "keyboardStepLarge", required: false }] }], storageKey: [{ type: i0.Input, args: [{ isSignal: true, alias: "storageKey", required: false }] }], rtl: [{ type: i0.Input, args: [{ isSignal: true, alias: "rtl", required: false }] }], sizesChange: [{ type: i0.Output, args: ["sizesChange"] }], resizeStart: [{ type: i0.Output, args: ["resizeStart"] }], resizeEnd: [{ type: i0.Output, args: ["resizeEnd"] }], collapseChange: [{ type: i0.Output, args: ["collapseChange"] }], _panes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => SplitPaneComponent), { isSignal: true }] }] } });
|
|
1037
|
+
function toPaneConfig(pane) {
|
|
1038
|
+
return {
|
|
1039
|
+
defaultSize: pane.defaultSize(),
|
|
1040
|
+
minSize: pane.minSize(),
|
|
1041
|
+
maxSize: pane.maxSize(),
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
/**
|
|
1046
|
+
* Marker directive for a custom gutter projection slot.
|
|
1047
|
+
* Attach to content inside `<tw-split>` to provide custom gutter visuals.
|
|
1048
|
+
* The container still owns all interaction logic.
|
|
1049
|
+
*/
|
|
1050
|
+
class SplitGutterDirective {
|
|
1051
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitGutterDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1052
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SplitGutterDirective, isStandalone: true, selector: "[twSplitGutter]", ngImport: i0 });
|
|
1053
|
+
}
|
|
1054
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitGutterDirective, decorators: [{
|
|
1055
|
+
type: Directive,
|
|
1056
|
+
args: [{
|
|
1057
|
+
selector: '[twSplitGutter]',
|
|
1058
|
+
}]
|
|
1059
|
+
}] });
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Marker directive for an optional header region inside `<tw-split-pane>`.
|
|
1063
|
+
* A parent Dock or Panel component may key off this region; the split pane
|
|
1064
|
+
* itself does not apply any styling to it.
|
|
1065
|
+
*/
|
|
1066
|
+
class SplitPaneHeaderDirective {
|
|
1067
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitPaneHeaderDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1068
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.2.14", type: SplitPaneHeaderDirective, isStandalone: true, selector: "[twSplitPaneHeader]", ngImport: i0 });
|
|
1069
|
+
}
|
|
1070
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: SplitPaneHeaderDirective, decorators: [{
|
|
1071
|
+
type: Directive,
|
|
1072
|
+
args: [{
|
|
1073
|
+
selector: '[twSplitPaneHeader]',
|
|
1074
|
+
}]
|
|
1075
|
+
}] });
|
|
1076
|
+
|
|
1077
|
+
/**
|
|
1078
|
+
* Generated bundle index. Do not edit.
|
|
1079
|
+
*/
|
|
1080
|
+
|
|
1081
|
+
export { SplitComponent, SplitGutterDirective, SplitPaneComponent, SplitPaneHeaderDirective };
|
|
1082
|
+
//# sourceMappingURL=cdevhub-ngx-tw-split.mjs.map
|