@ixfx/components 0.15.4 → 0.16.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/bundle/index.d.ts +1223 -48
- package/bundle/index.js +875 -700
- package/dist/ac-text.d.ts +1 -1
- package/dist/{ac-token-Cq7lsYf-.d.ts → ac-token-qEs3FsQv.d.ts} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
- package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
- package/dist/crumbs.d.ts +2 -115
- package/dist/crumbs.js +1 -1179
- package/dist/{data-grid-CbiubdG2.d.ts → data-grid-CIG2DVip.d.ts} +58 -9
- package/dist/{data-grid-Dx3l3dxw.js → data-grid-rMkLX2qo.js} +630 -215
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-ClkMV0SN.d.ts → detail-list-6iMTNP4N.d.ts} +35 -5
- package/dist/{detail-list-q8bs67BM.js → detail-list-C4pDSoI8.js} +218 -7
- package/dist/detail-list.d.ts +4 -4
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-0xTZdXzh.d.ts → flat-list-JPgQSmLW.d.ts} +28 -5
- package/dist/flat-list.d.ts +2 -2
- package/dist/flat-list.js +123 -4
- package/dist/{grid-list-y6dWJYu3.js → grid-list-CdEkScn-.js} +276 -75
- package/dist/{grid-list-BnWJUIBC.d.ts → grid-list-D6n7WH8l.d.ts} +128 -22
- package/dist/grid-list.d.ts +3 -3
- package/dist/grid-list.js +1 -1
- package/dist/{grouped-item-lister-Bhyo-dkH.d.ts → grouped-item-lister-DETte0Dh.d.ts} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +1 -1
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search.d.ts +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/{index-BAuqR9El.d.ts → index-Bx0BXPxI.d.ts} +52 -6
- package/dist/index-CYRc6Dah.d.ts +475 -0
- package/dist/{index-garxxqgV.d.ts → index-CYndvTWH.d.ts} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +1 -1
- package/dist/{index-Bz3loIAf.d.ts → index-DN2BWctP.d.ts} +1 -1
- package/dist/{index-BPTuCRGb.d.ts → index-Dr80Zdl6.d.ts} +2 -2
- package/dist/{index-cnaRPQPp.d.ts → index-DxyyVqzA.d.ts} +2 -2
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +1 -1
- package/dist/index.d.ts +380 -33
- package/dist/index.js +2247 -866
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/list-group-header.d.ts +1 -1
- package/dist/{list-native-drag-controller-O2Nruqpi.d.ts → list-native-drag-controller-D_UrXipS.d.ts} +1 -1
- package/dist/{list-selection-controller-BeE70lbB.js → list-selection-controller-5L0Nj9tR.js} +87 -2
- package/dist/{menu-item-B0zqw2xp.d.ts → menu-item-bF6narIz.d.ts} +1 -1
- package/dist/menu.d.ts +2 -2
- package/dist/miller-BFRfn0ve.js +2237 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- package/dist/panel.d.ts +1 -1
- package/dist/pending-BGidHppL.js +91 -0
- package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
- package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-CophkNeU.d.ts} +42 -4
- package/dist/{split-layout-Deoxy47d.js → split-layout-DTY4PJRp.js} +251 -19
- package/dist/split-layout.d.ts +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/sunburst-tree.d.ts +2 -0
- package/dist/sunburst-tree.js +2 -0
- package/dist/sunbursttree-list-B_VxOHlo.js +1556 -0
- package/dist/sunbursttree-list-D6j5BxVQ.d.ts +303 -0
- package/dist/{tab-list-wC0xNz1b.d.ts → tab-list-Df371rqw.d.ts} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/{titlebar-B5zSTZnF.d.ts → titlebar-Tqu-VvMU.d.ts} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/tree-CftHwtEb.js +146 -0
- package/dist/{tree-D8VxvR9u.js → tree-Crdpl4nc.js} +275 -47
- package/dist/{tree-component-C6UHO-w9.d.ts → tree-component-DgfkxmVY.d.ts} +19 -8
- package/dist/tree.d.ts +3 -3
- package/dist/tree.js +1 -1
- package/dist/types-LU1u0CB8.d.ts +87 -0
- package/dist/vertical-list-1V1Vg2O5.js +1993 -0
- package/dist/{vertical-list-B8FoU-pU.d.ts → vertical-list-BPDmZx6T.d.ts} +31 -3
- package/dist/vertical-list.d.ts +3 -3
- package/dist/vertical-list.js +1 -1789
- package/dist/view-state-controller-BdyXWUUM.js +305 -0
- package/docs-user/README.md +3 -0
- package/docs-user/crumbs.md +27 -0
- package/docs-user/data-grid.md +77 -5
- package/docs-user/detail-list.md +54 -0
- package/docs-user/dock.md +54 -12
- package/docs-user/flat-list.md +27 -0
- package/docs-user/grid-list.md +64 -11
- package/docs-user/hyperbolic-tree.md +31 -4
- package/docs-user/icicle-tree.md +36 -0
- package/docs-user/index.json +17 -2
- package/docs-user/llms.txt +3 -0
- package/docs-user/miller.md +52 -21
- package/docs-user/multicol-list.md +50 -8
- package/docs-user/split-layout.md +45 -17
- package/docs-user/sunburst-tree.md +223 -0
- package/docs-user/tree.md +46 -7
- package/docs-user/treemap.md +32 -0
- package/docs-user/twosplit-layout.md +22 -0
- package/docs-user/user-catalog.md +1 -1
- package/docs-user/vertical-list.md +55 -7
- package/llms.txt +3 -0
- package/package.json +5 -1
package/dist/index.js
CHANGED
|
@@ -18,7 +18,9 @@ import { t as prominenceStyles } from "./prominence-DlFShic3.js";
|
|
|
18
18
|
import { n as buttonStyles, t as ButtonElement } from "./button-ikByJJPN.js";
|
|
19
19
|
import { a as toGroupKey, i as resolveGroupKey, n as defaultGroupLabel, o as toggleInSet, r as partitionByGroup, t as EMPTY_GROUP_LABEL } from "./grouping-RYPnHkrS.js";
|
|
20
20
|
import { a as listItemFadeKeyframes, c as captionForEntry, i as animateListItemOut, l as createDefaultListDragPreview, n as LIST_ITEM_MOTION_MS, o as prefersReducedMotion, r as animateListItemIn, s as startListItemOutAnimation, t as ListNativeDragController } from "./list-native-drag-controller-CcXoS0YX.js";
|
|
21
|
-
import { n as isMultiMode, r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-
|
|
21
|
+
import { n as isMultiMode, r as ListDragSelectController, t as ListSelectionController } from "./list-selection-controller-5L0Nj9tR.js";
|
|
22
|
+
import { a as capKeys, c as keysAsFlags, d as partitionKeys, f as withoutFlag, i as capFlags, l as keysToArray, n as EMPTY_KEYS, o as flagsFromRecord, r as MAX_PENDING_KEYS, s as flagsToRecord, t as EMPTY_FLAGS, u as partitionFlags } from "./pending-BGidHppL.js";
|
|
23
|
+
import { n as createScrollHoverGuard, t as SCROLL_HOVER_SUPPRESS_ATTR } from "./scroll-hover-guard-Cjw6179Q.js";
|
|
22
24
|
import "./interaction-E-uMGHkJ.js";
|
|
23
25
|
import "./menu-item-BUv8An2k.js";
|
|
24
26
|
import { Checkbox } from "./checkbox.js";
|
|
@@ -26,35 +28,37 @@ import { t as CheckedSelect } from "./checked-select-CBUidG7n.js";
|
|
|
26
28
|
import { n as ColourPicker, t as ColourPickerPopup } from "./colour-picker-DwAa6RDh.js";
|
|
27
29
|
import { LabelledRangeInput } from "./labelled-range-input.js";
|
|
28
30
|
import { t as collectDataResult } from "./data-provider-35TyWclX.js";
|
|
29
|
-
import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-
|
|
31
|
+
import { a as TreeDataModel, c as pruneDraggedDescendants, i as TreeDropController, n as TreeListElement, o as applyTreeDrop, r as TreeBaseElement, s as dropTargetFor, t as TreeController } from "./tree-Crdpl4nc.js";
|
|
32
|
+
import { a as restoreElementScroll, c as VIEW_STATE_VERSION, i as onUserScrollIntent, n as anchorFromRow, o as scrollToOffset, r as captureElementScroll, s as VIEW_STATE_CHANGE, t as ViewStateController } from "./view-state-controller-BdyXWUUM.js";
|
|
30
33
|
import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
|
|
34
|
+
import { t as CrumbNavigationElement } from "./crumb-navigation-YnFVFqrx.js";
|
|
31
35
|
import { NarrowedTextElement } from "./narrowed-text.js";
|
|
32
|
-
import { CrumbNavigationElement } from "./crumbs.js";
|
|
33
36
|
import { DataDisplayComponent } from "./data-display.js";
|
|
34
37
|
import { t as createDragGhost } from "./drag-ghost-DtSDvjri.js";
|
|
35
|
-
import { t as DataGridElement } from "./data-grid-
|
|
38
|
+
import { t as DataGridElement } from "./data-grid-rMkLX2qo.js";
|
|
36
39
|
import { a as menuFromTreeToElements, t as menu_exports } from "./menu-CU9KAm8s.js";
|
|
37
40
|
import { t as PanTextElement } from "./pan-text-CqtvjPyK.js";
|
|
38
|
-
import { t as DetailListElement } from "./detail-list-
|
|
41
|
+
import { t as DetailListElement } from "./detail-list-C4pDSoI8.js";
|
|
39
42
|
import { ListGroupHeaderElement } from "./list-group-header.js";
|
|
40
|
-
import { a as distribute, c as preserveRestoreWeights, d as toSnapshot, f as walk, i as buildModel, l as resolveTracks, n as SplitPanelElement, o as findPanelNode, r as applySnapshot, s as normalise, t as SplitLayoutElement, u as setPanelRestoreWeight } from "./split-layout-
|
|
43
|
+
import { a as distribute, c as preserveRestoreWeights, d as toSnapshot, f as walk, i as buildModel, l as resolveTracks, n as SplitPanelElement, o as findPanelNode, r as applySnapshot, s as normalise, t as SplitLayoutElement, u as setPanelRestoreWeight } from "./split-layout-DTY4PJRp.js";
|
|
41
44
|
import { t as TabListElement } from "./tab-list-R_bGbDvO.js";
|
|
42
45
|
import { EditableLabel } from "./editable-label.js";
|
|
43
46
|
import { t as resolveAxisStep } from "./keyboard-B5xD7Fz-.js";
|
|
44
47
|
import { t as WheelNudgeMixin } from "./wheel-nudge-D5rmht3v.js";
|
|
45
48
|
import { n as EditableNumber, t as LabelledSliderInput } from "./labelled-slider-input-FdCRi6st.js";
|
|
46
|
-
import { VerticalListElement } from "./vertical-list.js";
|
|
49
|
+
import { t as VerticalListElement } from "./vertical-list-1V1Vg2O5.js";
|
|
47
50
|
import { FlatListElement } from "./flat-list.js";
|
|
48
51
|
import { n as formStyles, r as adoptLightDomStyles, t as FormElement } from "./form-CY0nibZ5.js";
|
|
49
|
-
import { t as GridListElement } from "./grid-list-
|
|
52
|
+
import { t as GridListElement } from "./grid-list-CdEkScn-.js";
|
|
50
53
|
import { PanelElement } from "./panel.js";
|
|
51
54
|
import { GroupedItemListerElement } from "./grouped-item-lister.js";
|
|
55
|
+
import { a as resolveKeyPath, i as reconcileSelection$1, n as reconcileCursor, t as pathFromJson } from "./tree-CftHwtEb.js";
|
|
52
56
|
import { t as IxfxHexEditorElement } from "./hex-editor-DiYpgIKs.js";
|
|
53
57
|
import { t as RadialInputElement } from "./radial-input-KMVltQfe.js";
|
|
54
58
|
import { LabelledRadialInput } from "./labelled-radial-input.js";
|
|
55
59
|
import { t as SliderInputElement } from "./slider-input-ByHdotzp.js";
|
|
56
60
|
import { LedElement } from "./led.js";
|
|
57
|
-
import {
|
|
61
|
+
import { n as MillerListElement, r as MillerBaseElement, t as MillerTreeController } from "./miller-BFRfn0ve.js";
|
|
58
62
|
import { NotificationPillElement } from "./notifier.js";
|
|
59
63
|
import { a as TooltipElement, c as computeColour, i as PlotMultiAxis, l as parseColourScaleAttr, n as PlotSingleAxis, o as PlotDataSeries, r as PlotHistogram, s as applyAgeModification, t as PlotXyAxis, u as readBaseColour } from "./xy-axis-CyMd_Bn1.js";
|
|
60
64
|
import { PolarPad } from "./polar-pad.js";
|
|
@@ -64,6 +68,7 @@ import { RangeElement } from "./range.js";
|
|
|
64
68
|
import { SelectHorizElement } from "./select-horiz.js";
|
|
65
69
|
import { SnackbarElement } from "./snackbar.js";
|
|
66
70
|
import { SnapContainer } from "./snap-container.js";
|
|
71
|
+
import { a as findSunburstId, c as sunburstChildId, d as sunburstParentId, f as sunburstPathToId, i as defaultSunburstValueAccessor, l as sunburstHasChildren, n as computeSunburstLayout, o as findSunburstNodeById, p as SUNBURST_PATH_SEPARATOR, r as createValueResolver, s as isSunburstAncestorId, t as SunburstTreeListElement, u as sunburstIdToPath } from "./sunbursttree-list-B_VxOHlo.js";
|
|
67
72
|
import { SwipeElement } from "./swipe.js";
|
|
68
73
|
import { a as resolveCssVars, i as attachTimelineInteractions, n as TimelineTrackTabElement, o as resolveGrid, r as timelineStyles, s as TimelineController, t as IxfxTimelineElement } from "./timeline-BP1Ux2EV.js";
|
|
69
74
|
import { TransitoryLabelElement } from "./transitory-label.js";
|
|
@@ -1407,6 +1412,46 @@ __decorate([state()], SplitButton.prototype, "_loadingItems", void 0);
|
|
|
1407
1412
|
__decorate([state()], SplitButton.prototype, "_menuHasItems", void 0);
|
|
1408
1413
|
SplitButton = __decorate([safeCustomElement(`ixfx-split-button`)], SplitButton);
|
|
1409
1414
|
//#endregion
|
|
1415
|
+
//#region src/common/view-state/bind.ts
|
|
1416
|
+
/** Whether `el` implements the view-state API. */
|
|
1417
|
+
function isViewStateful(el) {
|
|
1418
|
+
return typeof el === `object` && el !== null && typeof el.getViewState === `function` && typeof el.setViewState === `function`;
|
|
1419
|
+
}
|
|
1420
|
+
/**
|
|
1421
|
+
* Wire a component's view state to storage the host controls. Loads and
|
|
1422
|
+
* applies once, then saves on every `view-state-change` and when the page is
|
|
1423
|
+
* hidden. Returns a function that stops saving.
|
|
1424
|
+
*
|
|
1425
|
+
* ```ts
|
|
1426
|
+
* const stop = bindViewState(tree, {
|
|
1427
|
+
* load: () => JSON.parse(localStorage.getItem(`files`) ?? `null`),
|
|
1428
|
+
* save: s => localStorage.setItem(`files`, JSON.stringify(s)),
|
|
1429
|
+
* });
|
|
1430
|
+
* ```
|
|
1431
|
+
*/
|
|
1432
|
+
function bindViewState(el, binding) {
|
|
1433
|
+
let loaded;
|
|
1434
|
+
try {
|
|
1435
|
+
loaded = binding.load();
|
|
1436
|
+
} catch (error) {
|
|
1437
|
+
console.warn(`bindViewState: load failed`, error);
|
|
1438
|
+
}
|
|
1439
|
+
if (loaded) el.setViewState(loaded);
|
|
1440
|
+
const onChange = (event) => {
|
|
1441
|
+
const { detail } = event;
|
|
1442
|
+
if (event.target !== el) return;
|
|
1443
|
+
if (detail.reason === `restore` && !binding.saveOnRestore) return;
|
|
1444
|
+
binding.save(detail.state);
|
|
1445
|
+
};
|
|
1446
|
+
const onHide = () => binding.save(el.getViewState());
|
|
1447
|
+
el.addEventListener(VIEW_STATE_CHANGE, onChange);
|
|
1448
|
+
globalThis.addEventListener?.(`pagehide`, onHide);
|
|
1449
|
+
return () => {
|
|
1450
|
+
el.removeEventListener(VIEW_STATE_CHANGE, onChange);
|
|
1451
|
+
globalThis.removeEventListener?.(`pagehide`, onHide);
|
|
1452
|
+
};
|
|
1453
|
+
}
|
|
1454
|
+
//#endregion
|
|
1410
1455
|
//#region src/crumbs/crumb-path-controller.ts
|
|
1411
1456
|
var CrumbPathController = class {
|
|
1412
1457
|
#element = null;
|
|
@@ -3514,6 +3559,46 @@ function findEmptiedStacks(before, after) {
|
|
|
3514
3559
|
return emptied;
|
|
3515
3560
|
}
|
|
3516
3561
|
//#endregion
|
|
3562
|
+
//#region src/dock/dock-view-state.ts
|
|
3563
|
+
/**
|
|
3564
|
+
* Pure helpers for `ixfx-dock-host` view state. The dock's layout is close to
|
|
3565
|
+
* structure, so the saved state is the layout tree itself; these helpers
|
|
3566
|
+
* validate what comes back from storage and filter out panels the host
|
|
3567
|
+
* doesn't hold.
|
|
3568
|
+
*/
|
|
3569
|
+
/** Loose structural check of a parsed {@link DockLayout} (from storage, so untrusted). */
|
|
3570
|
+
function isDockLayout(value) {
|
|
3571
|
+
if (value === null || typeof value !== `object`) return false;
|
|
3572
|
+
return isDockNode(value.root);
|
|
3573
|
+
}
|
|
3574
|
+
function isDockNode(value) {
|
|
3575
|
+
if (value === null || typeof value !== `object`) return false;
|
|
3576
|
+
const node = value;
|
|
3577
|
+
if (node.kind === `stack`) return Array.isArray(node.panels) && node.panels.every((p) => typeof p === `string`) && typeof node.active === `string`;
|
|
3578
|
+
if (node.kind === `split`) return (node.direction === `row` || node.direction === `column`) && Array.isArray(node.sizes) && node.sizes.every((n) => typeof n === `number`) && Array.isArray(node.children) && node.children.every(isDockNode);
|
|
3579
|
+
return false;
|
|
3580
|
+
}
|
|
3581
|
+
/**
|
|
3582
|
+
* `layout` with every panel `isPresent` rejects removed (stacks that empty
|
|
3583
|
+
* out are dropped unless named, splits collapse as usual). Used for
|
|
3584
|
+
* `getViewState({ includePending: false })`.
|
|
3585
|
+
*/
|
|
3586
|
+
function withoutAbsentPanels(layout, isPresent) {
|
|
3587
|
+
const root = serializeLayout(layout.root).root;
|
|
3588
|
+
const visit = (node) => {
|
|
3589
|
+
if (isStack(node)) {
|
|
3590
|
+
node.panels = node.panels.filter(isPresent);
|
|
3591
|
+
return;
|
|
3592
|
+
}
|
|
3593
|
+
node.children.forEach(visit);
|
|
3594
|
+
};
|
|
3595
|
+
visit(root);
|
|
3596
|
+
return {
|
|
3597
|
+
version: 1,
|
|
3598
|
+
root: normalize(root)
|
|
3599
|
+
};
|
|
3600
|
+
}
|
|
3601
|
+
//#endregion
|
|
3517
3602
|
//#region src/tabs/tab-list-item.ts
|
|
3518
3603
|
const resizeHandlers = /* @__PURE__ */ new WeakMap();
|
|
3519
3604
|
const sharedResizeObserver = new ResizeObserver((entries) => {
|
|
@@ -4158,6 +4243,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4158
4243
|
this._toolbarSlots = /* @__PURE__ */ new Set();
|
|
4159
4244
|
this._toolbarObserver = null;
|
|
4160
4245
|
this._panelObservers = /* @__PURE__ */ new Map();
|
|
4246
|
+
this.#viewState = new ViewStateController(this, {
|
|
4247
|
+
kind: `dock-host`,
|
|
4248
|
+
capture: () => this.getViewState()
|
|
4249
|
+
});
|
|
4250
|
+
this.#layoutRestored = false;
|
|
4161
4251
|
this._onDragStart = (event) => {
|
|
4162
4252
|
if (event.composedPath().find((n) => n instanceof HTMLElement && n.tagName === `IXFX-DOCK-HOST`) !== this) return;
|
|
4163
4253
|
const { panelId, pointerId, clientX, clientY } = event.detail;
|
|
@@ -4190,6 +4280,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4190
4280
|
this._onSplitWeightsChanged = () => {
|
|
4191
4281
|
this._foldLiveWeights();
|
|
4192
4282
|
this._scheduleJunctions();
|
|
4283
|
+
this.#viewState.notify();
|
|
4284
|
+
};
|
|
4285
|
+
this._onInnerViewState = (event) => {
|
|
4286
|
+
if (event.target === event.currentTarget) event.stopPropagation();
|
|
4193
4287
|
};
|
|
4194
4288
|
this._onSplitCollapsed = (event, collapsed) => {
|
|
4195
4289
|
const detail = event.detail;
|
|
@@ -4200,6 +4294,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4200
4294
|
if (node === null || !isStack(node)) return;
|
|
4201
4295
|
if (collapsed) node.collapsed = true;
|
|
4202
4296
|
else delete node.collapsed;
|
|
4297
|
+
this.#viewState.notify();
|
|
4203
4298
|
};
|
|
4204
4299
|
this._onSplitClose = (event) => {
|
|
4205
4300
|
event.preventDefault();
|
|
@@ -4222,6 +4317,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4222
4317
|
if (stack === null || !isStack(stack)) return;
|
|
4223
4318
|
if (!setActive(stack, panelId)) return;
|
|
4224
4319
|
this.requestUpdate();
|
|
4320
|
+
this.#viewState.notify();
|
|
4225
4321
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4226
4322
|
detail: { panelId },
|
|
4227
4323
|
bubbles: true,
|
|
@@ -4272,6 +4368,9 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4272
4368
|
this._teardownTabDragListeners();
|
|
4273
4369
|
};
|
|
4274
4370
|
}
|
|
4371
|
+
#viewState;
|
|
4372
|
+
/** A layout was restored via `setViewState()`; it wins over the initial `layout` attribute. */
|
|
4373
|
+
#layoutRestored;
|
|
4275
4374
|
connectedCallback() {
|
|
4276
4375
|
super.connectedCallback();
|
|
4277
4376
|
this._buildInitialModel();
|
|
@@ -4309,7 +4408,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4309
4408
|
}
|
|
4310
4409
|
willUpdate(changed) {
|
|
4311
4410
|
super.willUpdate(changed);
|
|
4312
|
-
|
|
4411
|
+
const initial = !this.hasUpdated && this.#layoutRestored;
|
|
4412
|
+
if (changed.has(`layout`) && this.layout !== null && !initial) {
|
|
4313
4413
|
const parsed = parseLayout(this.layout);
|
|
4314
4414
|
if (parsed) {
|
|
4315
4415
|
this._model = parsed.root;
|
|
@@ -4341,6 +4441,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4341
4441
|
this._scheduleJunctions();
|
|
4342
4442
|
}
|
|
4343
4443
|
_buildInitialModel() {
|
|
4444
|
+
if (this.#layoutRestored) {
|
|
4445
|
+
this._mergePoolIntoModel();
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4344
4448
|
if (this.layout !== null) {
|
|
4345
4449
|
const parsed = parseLayout(this.layout);
|
|
4346
4450
|
if (parsed) {
|
|
@@ -4475,21 +4579,56 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4475
4579
|
}
|
|
4476
4580
|
this._pool = current;
|
|
4477
4581
|
this.requestUpdate();
|
|
4478
|
-
if (structuralChange) this._emitChange();
|
|
4582
|
+
if (structuralChange) this._emitChange(`data`);
|
|
4583
|
+
}
|
|
4584
|
+
/**
|
|
4585
|
+
* Snapshot the layout (live split weights folded in from the DOM). With
|
|
4586
|
+
* `includePending` (default), panels the restored layout names but the
|
|
4587
|
+
* host doesn't hold yet are kept in place; without it they are left out.
|
|
4588
|
+
*/
|
|
4589
|
+
getViewState(options) {
|
|
4590
|
+
const layout = this._captureLayout();
|
|
4591
|
+
const withPending = options?.includePending ?? true;
|
|
4592
|
+
return this.#viewState.shape({ layout: withPending ? layout : withoutAbsentPanels(layout, (id) => this._findPanelEl(id) !== void 0) }, options);
|
|
4593
|
+
}
|
|
4594
|
+
/**
|
|
4595
|
+
* Restore a layout. Panels it names that aren't in the host yet keep their
|
|
4596
|
+
* place (shown as a placeholder tab) and slot in when they arrive — as a
|
|
4597
|
+
* light-DOM child, or via `addPanel()` without a target. Pooled panels it
|
|
4598
|
+
* doesn't name are appended to the first stack admitting their kind.
|
|
4599
|
+
*/
|
|
4600
|
+
setViewState(state) {
|
|
4601
|
+
if (!this.#viewState.accepts(state) || state.layout === void 0) return;
|
|
4602
|
+
if (!isDockLayout(state.layout)) {
|
|
4603
|
+
console.warn(`[dock-host] ignoring view state with a malformed layout`);
|
|
4604
|
+
return;
|
|
4605
|
+
}
|
|
4606
|
+
const layout = state.layout;
|
|
4607
|
+
this.#viewState.restoring(() => {
|
|
4608
|
+
this.#layoutRestored = true;
|
|
4609
|
+
this._returnAllToolbars();
|
|
4610
|
+
this._loadGeneration++;
|
|
4611
|
+
this._model = normalize(cloneNode(layout.root));
|
|
4612
|
+
this._mergePoolIntoModel();
|
|
4613
|
+
this._liveStacks = snapshotNonEmptyStacks(this._model);
|
|
4614
|
+
this.requestUpdate();
|
|
4615
|
+
this._emitChange();
|
|
4616
|
+
});
|
|
4479
4617
|
}
|
|
4480
|
-
/**
|
|
4481
|
-
|
|
4618
|
+
/**
|
|
4619
|
+
* Placeholders are part of the layout tree rather than a separate pending
|
|
4620
|
+
* set, so there is nothing to clear here: remove a placeholder with
|
|
4621
|
+
* `removePanel(id)`.
|
|
4622
|
+
*/
|
|
4623
|
+
clearPendingViewState() {}
|
|
4624
|
+
/** The current layout, folding live split weights from the DOM. */
|
|
4625
|
+
_captureLayout() {
|
|
4482
4626
|
this._foldLiveWeights();
|
|
4483
4627
|
return serializeLayout(this._model);
|
|
4484
4628
|
}
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
this.
|
|
4488
|
-
this._model = normalize(cloneNode(layout.root));
|
|
4489
|
-
this._mergePoolIntoModel();
|
|
4490
|
-
this._liveStacks = snapshotNonEmptyStacks(this._model);
|
|
4491
|
-
this.requestUpdate();
|
|
4492
|
-
this._emitChange();
|
|
4629
|
+
/** Whether `panelId` is in the model only as a placeholder from a restored layout. */
|
|
4630
|
+
_isPlaceholder(panelId) {
|
|
4631
|
+
return !this._pool.some((p) => p.panelId === panelId) && collectPanelIds(this._model).includes(panelId);
|
|
4493
4632
|
}
|
|
4494
4633
|
/**
|
|
4495
4634
|
* Dock a panel. Returns false (without touching the DOM) when the
|
|
@@ -4536,11 +4675,12 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4536
4675
|
if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
|
|
4537
4676
|
if (panel.kind !== void 0) el.kind = panel.kind;
|
|
4538
4677
|
}
|
|
4678
|
+
const takesSavedPlace = target === void 0 && this._isPlaceholder(el.panelId);
|
|
4539
4679
|
if (el.parentElement !== this) this.appendChild(el);
|
|
4540
4680
|
if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
|
|
4541
4681
|
this._returnAllToolbars();
|
|
4542
|
-
if (collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
|
|
4543
|
-
if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
|
|
4682
|
+
if (!takesSavedPlace && collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
|
|
4683
|
+
if (takesSavedPlace) {} else if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
|
|
4544
4684
|
else if (stackPath !== null) {
|
|
4545
4685
|
const stack = findNode(this._model, stackPath);
|
|
4546
4686
|
if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target?.index);
|
|
@@ -4581,6 +4721,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4581
4721
|
if (stack === null || !isStack(stack)) return false;
|
|
4582
4722
|
if (!setActive(stack, panelId)) return false;
|
|
4583
4723
|
this.requestUpdate();
|
|
4724
|
+
this.#viewState.notify();
|
|
4584
4725
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4585
4726
|
detail: { panelId },
|
|
4586
4727
|
bubbles: true,
|
|
@@ -4640,7 +4781,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4640
4781
|
* {@link setLeafPlacementPolicy}'s `allowEdgeSplit` (default `true`).
|
|
4641
4782
|
* Pass `[]` to seal the leaf (nothing admitted). Warns when no stack
|
|
4642
4783
|
* currently carries `name`, but the rule is still stored and applies once
|
|
4643
|
-
* one appears. Rules are host policy: kept across `
|
|
4784
|
+
* one appears. Rules are host policy: kept across `setViewState()`, never
|
|
4644
4785
|
* serialized.
|
|
4645
4786
|
*/
|
|
4646
4787
|
setLeafExclusiveContents(name, kinds) {
|
|
@@ -4687,7 +4828,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4687
4828
|
* unconstrained* leaf — the policy is not propagated to it.
|
|
4688
4829
|
*
|
|
4689
4830
|
* Warns when no stack currently carries `name`; the policy is stored and
|
|
4690
|
-
* applies once one appears. Host policy: kept across `
|
|
4831
|
+
* applies once one appears. Host policy: kept across `setViewState()`, never
|
|
4691
4832
|
* serialized.
|
|
4692
4833
|
*/
|
|
4693
4834
|
setLeafPlacementPolicy(name, policy) {
|
|
@@ -4836,7 +4977,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4836
4977
|
* already docked here or its kind is refused by `acceptsKinds`.
|
|
4837
4978
|
*/
|
|
4838
4979
|
acceptExternalPanel(snapshot, at) {
|
|
4839
|
-
|
|
4980
|
+
const placeholder = this._isPlaceholder(snapshot.panelId);
|
|
4981
|
+
if (!placeholder && collectPanelIds(this._model).includes(snapshot.panelId)) return null;
|
|
4840
4982
|
if (this.acceptsKinds !== null && !this.acceptsKinds.includes(normalizeKind(snapshot.kind))) return null;
|
|
4841
4983
|
const el = document.createElement(`ixfx-dock-panel`);
|
|
4842
4984
|
el.panelId = snapshot.panelId;
|
|
@@ -4849,7 +4991,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4849
4991
|
if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
|
|
4850
4992
|
this._returnAllToolbars();
|
|
4851
4993
|
this._pool = [...this._pool, el];
|
|
4852
|
-
this._appendPanelToFirstStack(el.panelId);
|
|
4994
|
+
if (!placeholder) this._appendPanelToFirstStack(el.panelId);
|
|
4853
4995
|
this.appendChild(el);
|
|
4854
4996
|
normalize(this._model);
|
|
4855
4997
|
this.endExternalDrop();
|
|
@@ -5345,10 +5487,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5345
5487
|
hostEmpty: now.size === 0
|
|
5346
5488
|
});
|
|
5347
5489
|
}
|
|
5348
|
-
_emitChange() {
|
|
5349
|
-
const layout = this.
|
|
5490
|
+
_emitChange(reason = `user`) {
|
|
5491
|
+
const layout = this._captureLayout();
|
|
5350
5492
|
this._announceEmptiedStacks();
|
|
5351
5493
|
this._emit(`dock-change`, { layout });
|
|
5494
|
+
this.#viewState.notify(reason);
|
|
5352
5495
|
}
|
|
5353
5496
|
render() {
|
|
5354
5497
|
return html`
|
|
@@ -5384,6 +5527,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5384
5527
|
return html`
|
|
5385
5528
|
<ixfx-split-layout
|
|
5386
5529
|
class="split"
|
|
5530
|
+
forget-absent
|
|
5387
5531
|
direction=${node.direction}
|
|
5388
5532
|
size=${size === void 0 ? nothing : live(String(size))}
|
|
5389
5533
|
style=${`--split-gutter-size: var(--dock-split-bar-size, 2px); --split-collapsed-size: var(--dock-collapsed-size, 40px)`}
|
|
@@ -5394,6 +5538,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5394
5538
|
@ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
|
|
5395
5539
|
@ixfx-split-expand=${(e) => this._onSplitCollapsed(e, false)}
|
|
5396
5540
|
@ixfx-split-close=${this._onSplitClose}
|
|
5541
|
+
@view-state-change=${this._onInnerViewState}
|
|
5397
5542
|
>
|
|
5398
5543
|
${node.children.map((child, i) => this._renderNode(child, [...path, i], node.sizes[i]))}
|
|
5399
5544
|
</ixfx-split-layout>
|
|
@@ -6767,269 +6912,6 @@ function createGestureHandlers(host) {
|
|
|
6767
6912
|
};
|
|
6768
6913
|
}
|
|
6769
6914
|
//#endregion
|
|
6770
|
-
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
6771
|
-
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
6772
|
-
const hyperbolicTreeStyles = css`
|
|
6773
|
-
:host {
|
|
6774
|
-
--hyperbolictree-node-color: var(--surface-4);
|
|
6775
|
-
--hyperbolictree-node-text: var(--surface-3-text);
|
|
6776
|
-
--hyperbolictree-edge-color: var(--surface-6);
|
|
6777
|
-
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
6778
|
-
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
6779
|
-
--hyperbolictree-font-size: 13px;
|
|
6780
|
-
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
6781
|
-
--hyperbolictree-box-text: var(--surface-4-text);
|
|
6782
|
-
--hyperbolictree-box-border: var(--surface-2);
|
|
6783
|
-
--hyperbolictree-branch-hue-start: 250;
|
|
6784
|
-
--hyperbolictree-branch-chroma: 0.13;
|
|
6785
|
-
--hyperbolictree-branch-lightness: 62%;
|
|
6786
|
-
--hyperbolictree-branch-mix: 55%;
|
|
6787
|
-
|
|
6788
|
-
display: block;
|
|
6789
|
-
position: relative;
|
|
6790
|
-
overflow: hidden;
|
|
6791
|
-
outline: none;
|
|
6792
|
-
overscroll-behavior: contain;
|
|
6793
|
-
}
|
|
6794
|
-
|
|
6795
|
-
/*
|
|
6796
|
-
* delegatesFocus means the host itself reports as focused whenever
|
|
6797
|
-
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
6798
|
-
* the host also picks up the browser's own default focus ring/fill
|
|
6799
|
-
* sized to the whole box.
|
|
6800
|
-
*/
|
|
6801
|
-
:host(:focus),
|
|
6802
|
-
:host(:focus-visible) {
|
|
6803
|
-
outline: none;
|
|
6804
|
-
}
|
|
6805
|
-
|
|
6806
|
-
.hyperbolictree {
|
|
6807
|
-
display: block;
|
|
6808
|
-
width: 100%;
|
|
6809
|
-
height: 100%;
|
|
6810
|
-
outline: none;
|
|
6811
|
-
touch-action: none;
|
|
6812
|
-
overscroll-behavior: contain;
|
|
6813
|
-
}
|
|
6814
|
-
|
|
6815
|
-
.disk-boundary {
|
|
6816
|
-
fill: none;
|
|
6817
|
-
stroke: var(--hyperbolictree-boundary-color);
|
|
6818
|
-
stroke-width: 1;
|
|
6819
|
-
}
|
|
6820
|
-
|
|
6821
|
-
.edge {
|
|
6822
|
-
fill: none;
|
|
6823
|
-
stroke: var(--hyperbolictree-edge-color);
|
|
6824
|
-
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
6825
|
-
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
6826
|
-
/* Constant on-screen width, however far the disk is zoomed. */
|
|
6827
|
-
vector-effect: non-scaling-stroke;
|
|
6828
|
-
}
|
|
6829
|
-
|
|
6830
|
-
.node {
|
|
6831
|
-
cursor: pointer;
|
|
6832
|
-
}
|
|
6833
|
-
|
|
6834
|
-
/*
|
|
6835
|
-
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
6836
|
-
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
6837
|
-
* so it stays in keeping with — and legible in — the current theme.
|
|
6838
|
-
* Only sets the base colour vars, so selected/tickled states still win.
|
|
6839
|
-
*/
|
|
6840
|
-
:host([color-by-branch]) .node,
|
|
6841
|
-
:host([color-by-branch]) .edge {
|
|
6842
|
-
--_branch-color: oklch(
|
|
6843
|
-
var(--hyperbolictree-branch-lightness)
|
|
6844
|
-
var(--hyperbolictree-branch-chroma)
|
|
6845
|
-
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
6846
|
-
);
|
|
6847
|
-
}
|
|
6848
|
-
|
|
6849
|
-
:host([color-by-branch]) .node {
|
|
6850
|
-
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6851
|
-
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6852
|
-
}
|
|
6853
|
-
|
|
6854
|
-
:host([color-by-branch]) .edge {
|
|
6855
|
-
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
6856
|
-
}
|
|
6857
|
-
|
|
6858
|
-
.node-hit {
|
|
6859
|
-
fill: transparent;
|
|
6860
|
-
pointer-events: all;
|
|
6861
|
-
}
|
|
6862
|
-
|
|
6863
|
-
.node-dot {
|
|
6864
|
-
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
6865
|
-
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
6866
|
-
stroke: var(--surface-2);
|
|
6867
|
-
stroke-width: 1;
|
|
6868
|
-
pointer-events: none;
|
|
6869
|
-
}
|
|
6870
|
-
|
|
6871
|
-
.node-box {
|
|
6872
|
-
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
6873
|
-
stroke: var(--hyperbolictree-box-border);
|
|
6874
|
-
stroke-width: 1.5;
|
|
6875
|
-
/* Keep the border crisp however far the box has been scaled down. */
|
|
6876
|
-
vector-effect: non-scaling-stroke;
|
|
6877
|
-
}
|
|
6878
|
-
|
|
6879
|
-
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
6880
|
-
.node-box.marker {
|
|
6881
|
-
stroke-width: 1;
|
|
6882
|
-
}
|
|
6883
|
-
|
|
6884
|
-
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
6885
|
-
.hyperbolictree-label {
|
|
6886
|
-
fill: var(--hyperbolictree-node-text);
|
|
6887
|
-
cursor: pointer;
|
|
6888
|
-
user-select: none;
|
|
6889
|
-
}
|
|
6890
|
-
|
|
6891
|
-
.box-label {
|
|
6892
|
-
fill: var(--hyperbolictree-box-text);
|
|
6893
|
-
pointer-events: none;
|
|
6894
|
-
}
|
|
6895
|
-
|
|
6896
|
-
/*
|
|
6897
|
-
* State fills come paired with their theme text tokens: a box label sits
|
|
6898
|
-
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
6899
|
-
* --item-text-selected on top, not the resting box text colour.
|
|
6900
|
-
*/
|
|
6901
|
-
.node.tickled .node-dot,
|
|
6902
|
-
.node.tickled .node-box {
|
|
6903
|
-
fill: var(--item-bg-tickled);
|
|
6904
|
-
}
|
|
6905
|
-
|
|
6906
|
-
.node.tickled .box-label {
|
|
6907
|
-
fill: var(--item-text-tickled);
|
|
6908
|
-
}
|
|
6909
|
-
|
|
6910
|
-
.node.selected .node-dot,
|
|
6911
|
-
.node.selected .node-box {
|
|
6912
|
-
fill: var(--item-bg-selected);
|
|
6913
|
-
}
|
|
6914
|
-
|
|
6915
|
-
.node.selected .box-label {
|
|
6916
|
-
fill: var(--item-text-selected);
|
|
6917
|
-
}
|
|
6918
|
-
|
|
6919
|
-
.node.selected.tickled .node-dot,
|
|
6920
|
-
.node.selected.tickled .node-box {
|
|
6921
|
-
fill: var(--item-bg-selected-tickled);
|
|
6922
|
-
}
|
|
6923
|
-
|
|
6924
|
-
.node.focused .node-dot,
|
|
6925
|
-
.node.focused .node-box {
|
|
6926
|
-
stroke: var(--accent);
|
|
6927
|
-
stroke-width: 3;
|
|
6928
|
-
}
|
|
6929
|
-
|
|
6930
|
-
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
6931
|
-
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
6932
|
-
paint-order: stroke;
|
|
6933
|
-
stroke: var(--surface-2);
|
|
6934
|
-
stroke-width: 4px;
|
|
6935
|
-
stroke-linejoin: round;
|
|
6936
|
-
}
|
|
6937
|
-
|
|
6938
|
-
.node.filtered {
|
|
6939
|
-
opacity: 0.3;
|
|
6940
|
-
}
|
|
6941
|
-
|
|
6942
|
-
.node.loading .node-dot,
|
|
6943
|
-
.node.loading .node-box {
|
|
6944
|
-
opacity: 0.6;
|
|
6945
|
-
}
|
|
6946
|
-
|
|
6947
|
-
|
|
6948
|
-
/* Child preview: a clickable list of a crowded node's children. */
|
|
6949
|
-
.child-preview {
|
|
6950
|
-
position: absolute;
|
|
6951
|
-
z-index: 1;
|
|
6952
|
-
min-width: 8rem;
|
|
6953
|
-
max-width: min(18rem, 60%);
|
|
6954
|
-
max-height: 45%;
|
|
6955
|
-
overflow: auto;
|
|
6956
|
-
padding: var(--space-xs, 4px);
|
|
6957
|
-
background: var(--surface-3);
|
|
6958
|
-
color: var(--surface-3-text);
|
|
6959
|
-
border: var(--border-subtle);
|
|
6960
|
-
border-radius: var(--radius-m, 6px);
|
|
6961
|
-
box-shadow: var(--shadow-m);
|
|
6962
|
-
font-size: var(--hyperbolictree-font-size);
|
|
6963
|
-
/* Beside the node, never over it, with the dot level with the popover's middle. */
|
|
6964
|
-
transform: translate(14px, -50%);
|
|
6965
|
-
}
|
|
6966
|
-
|
|
6967
|
-
.child-preview.left {
|
|
6968
|
-
transform: translate(calc(-100% - 14px), -50%);
|
|
6969
|
-
}
|
|
6970
|
-
|
|
6971
|
-
.child-preview.below {
|
|
6972
|
-
transform: translate(-50%, 14px);
|
|
6973
|
-
}
|
|
6974
|
-
|
|
6975
|
-
.child-preview.above {
|
|
6976
|
-
transform: translate(-50%, calc(-100% - 14px));
|
|
6977
|
-
}
|
|
6978
|
-
|
|
6979
|
-
.child-preview-title {
|
|
6980
|
-
display: flex;
|
|
6981
|
-
align-items: center;
|
|
6982
|
-
gap: 4px;
|
|
6983
|
-
padding: 2px 6px 4px;
|
|
6984
|
-
font-weight: 600;
|
|
6985
|
-
}
|
|
6986
|
-
|
|
6987
|
-
.child-preview-title span {
|
|
6988
|
-
overflow: hidden;
|
|
6989
|
-
text-overflow: ellipsis;
|
|
6990
|
-
white-space: nowrap;
|
|
6991
|
-
}
|
|
6992
|
-
|
|
6993
|
-
.child-preview button.child-preview-up {
|
|
6994
|
-
display: inline-flex;
|
|
6995
|
-
flex: none;
|
|
6996
|
-
width: auto;
|
|
6997
|
-
padding: 2px;
|
|
6998
|
-
}
|
|
6999
|
-
|
|
7000
|
-
.child-preview ul {
|
|
7001
|
-
margin: 0;
|
|
7002
|
-
padding: 0;
|
|
7003
|
-
list-style: none;
|
|
7004
|
-
}
|
|
7005
|
-
|
|
7006
|
-
.child-preview button {
|
|
7007
|
-
display: block;
|
|
7008
|
-
width: 100%;
|
|
7009
|
-
padding: 2px 6px;
|
|
7010
|
-
border: none;
|
|
7011
|
-
border-radius: var(--radius-s, 4px);
|
|
7012
|
-
background: none;
|
|
7013
|
-
color: inherit;
|
|
7014
|
-
font: inherit;
|
|
7015
|
-
text-align: start;
|
|
7016
|
-
overflow: hidden;
|
|
7017
|
-
text-overflow: ellipsis;
|
|
7018
|
-
white-space: nowrap;
|
|
7019
|
-
cursor: pointer;
|
|
7020
|
-
}
|
|
7021
|
-
|
|
7022
|
-
.child-preview button:hover {
|
|
7023
|
-
background: var(--item-bg-tickled);
|
|
7024
|
-
color: var(--item-text-tickled);
|
|
7025
|
-
}
|
|
7026
|
-
|
|
7027
|
-
.child-preview-more {
|
|
7028
|
-
padding: 2px 6px;
|
|
7029
|
-
color: var(--surface-muted-text);
|
|
7030
|
-
}
|
|
7031
|
-
`;
|
|
7032
|
-
//#endregion
|
|
7033
6915
|
//#region src/hyperbolic-tree/layout.ts
|
|
7034
6916
|
const ID_SEPARATOR = `\u001F`;
|
|
7035
6917
|
/**
|
|
@@ -7444,6 +7326,564 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
7444
7326
|
};
|
|
7445
7327
|
}
|
|
7446
7328
|
//#endregion
|
|
7329
|
+
//#region src/hyperbolic-tree/hyperbolic-view-state.ts
|
|
7330
|
+
function indexHyperbolicKeys(root) {
|
|
7331
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
7332
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
7333
|
+
const walk = (node) => {
|
|
7334
|
+
nodes.add(node);
|
|
7335
|
+
if (!byKey.has(node.item.key)) byKey.set(node.item.key, node);
|
|
7336
|
+
for (const child of node.children ?? []) walk(child);
|
|
7337
|
+
};
|
|
7338
|
+
if (root) walk(root);
|
|
7339
|
+
return {
|
|
7340
|
+
byKey,
|
|
7341
|
+
nodes
|
|
7342
|
+
};
|
|
7343
|
+
}
|
|
7344
|
+
/** Key path from the root's children to the node with layout id `id`. */
|
|
7345
|
+
function centerPathOf(root, id) {
|
|
7346
|
+
if (id === void 0) return [];
|
|
7347
|
+
return findHyperbolicPathById(root, id)?.slice(1).map((n) => n.item.key) ?? [];
|
|
7348
|
+
}
|
|
7349
|
+
/** Walk `path` from the root, matching each step by key among the loaded children. */
|
|
7350
|
+
function resolveCenterPath(root, path) {
|
|
7351
|
+
if (!root) return { kind: `missing` };
|
|
7352
|
+
let node = root;
|
|
7353
|
+
let id = root.item.key;
|
|
7354
|
+
for (const [depth, key] of path.entries()) {
|
|
7355
|
+
const children = node.children;
|
|
7356
|
+
if (children === void 0) return node.item.isLeaf === true ? { kind: `missing` } : {
|
|
7357
|
+
kind: `load`,
|
|
7358
|
+
node,
|
|
7359
|
+
depth
|
|
7360
|
+
};
|
|
7361
|
+
const index = children.findIndex((c) => c.item.key === key);
|
|
7362
|
+
if (index < 0) return { kind: `missing` };
|
|
7363
|
+
id = hyperbolicChildIds(id, children)[index];
|
|
7364
|
+
node = children[index];
|
|
7365
|
+
}
|
|
7366
|
+
return {
|
|
7367
|
+
kind: `found`,
|
|
7368
|
+
id: path.length === 0 ? void 0 : id
|
|
7369
|
+
};
|
|
7370
|
+
}
|
|
7371
|
+
/**
|
|
7372
|
+
* As `reconcileSelection` in `tree/tree-view-state.ts`, but a selected node
|
|
7373
|
+
* still in the tree is kept by identity — keys may repeat here, so re-keying
|
|
7374
|
+
* it would jump the selection to the first node sharing its key on every
|
|
7375
|
+
* lazy load. Only nodes that left the tree are re-keyed (or go pending).
|
|
7376
|
+
*/
|
|
7377
|
+
function reconcileHyperbolicSelection(selected, pending, index, canSelect, single) {
|
|
7378
|
+
if (selected.size === 0 && pending.size === 0) return {
|
|
7379
|
+
selected,
|
|
7380
|
+
pending,
|
|
7381
|
+
restored: false,
|
|
7382
|
+
changed: false
|
|
7383
|
+
};
|
|
7384
|
+
const next = /* @__PURE__ */ new Set();
|
|
7385
|
+
const nextPending = /* @__PURE__ */ new Set();
|
|
7386
|
+
let changed = false;
|
|
7387
|
+
for (const node of selected) {
|
|
7388
|
+
if (index.nodes.has(node)) {
|
|
7389
|
+
next.add(node);
|
|
7390
|
+
continue;
|
|
7391
|
+
}
|
|
7392
|
+
changed = true;
|
|
7393
|
+
const current = index.byKey.get(node.item.key);
|
|
7394
|
+
if (current) next.add(current);
|
|
7395
|
+
else nextPending.add(node.item.key);
|
|
7396
|
+
}
|
|
7397
|
+
let restored;
|
|
7398
|
+
for (const key of pending) {
|
|
7399
|
+
const node = index.byKey.get(key);
|
|
7400
|
+
if (node && canSelect(node)) {
|
|
7401
|
+
next.add(node);
|
|
7402
|
+
restored = node;
|
|
7403
|
+
changed = true;
|
|
7404
|
+
} else nextPending.add(key);
|
|
7405
|
+
}
|
|
7406
|
+
if (!changed) return {
|
|
7407
|
+
selected,
|
|
7408
|
+
pending,
|
|
7409
|
+
restored: false,
|
|
7410
|
+
changed: false
|
|
7411
|
+
};
|
|
7412
|
+
if (single && next.size > 0) return {
|
|
7413
|
+
selected: /* @__PURE__ */ new Set([restored ?? [...next].at(-1)]),
|
|
7414
|
+
pending: EMPTY_KEYS,
|
|
7415
|
+
restored: restored !== void 0,
|
|
7416
|
+
changed: true
|
|
7417
|
+
};
|
|
7418
|
+
return {
|
|
7419
|
+
selected: next,
|
|
7420
|
+
pending: capKeys(nextPending),
|
|
7421
|
+
restored: restored !== void 0,
|
|
7422
|
+
changed: true
|
|
7423
|
+
};
|
|
7424
|
+
}
|
|
7425
|
+
/**
|
|
7426
|
+
* Centre: a live centre whose path no longer resolves goes pending (the view
|
|
7427
|
+
* falls back to the root); one that resolves to a new id is re-seated; a
|
|
7428
|
+
* pending path that now resolves is applied.
|
|
7429
|
+
*/
|
|
7430
|
+
function reconcileHyperbolicViewState(input) {
|
|
7431
|
+
const { root } = input;
|
|
7432
|
+
let center;
|
|
7433
|
+
let pendingCenter = input.pendingCenter;
|
|
7434
|
+
if (input.focusId !== void 0) {
|
|
7435
|
+
const live = resolveCenterPath(root, input.centerPath);
|
|
7436
|
+
if (live.kind !== `found` || live.id === void 0) {
|
|
7437
|
+
pendingCenter ??= input.centerPath;
|
|
7438
|
+
center = {
|
|
7439
|
+
id: void 0,
|
|
7440
|
+
path: [],
|
|
7441
|
+
restored: false
|
|
7442
|
+
};
|
|
7443
|
+
} else if (live.id !== input.focusId) center = {
|
|
7444
|
+
id: live.id,
|
|
7445
|
+
path: input.centerPath,
|
|
7446
|
+
restored: false
|
|
7447
|
+
};
|
|
7448
|
+
}
|
|
7449
|
+
if (pendingCenter) {
|
|
7450
|
+
const resolved = resolveCenterPath(root, pendingCenter);
|
|
7451
|
+
if (resolved.kind === `found`) {
|
|
7452
|
+
center = {
|
|
7453
|
+
id: resolved.id,
|
|
7454
|
+
path: pendingCenter,
|
|
7455
|
+
restored: true
|
|
7456
|
+
};
|
|
7457
|
+
pendingCenter = void 0;
|
|
7458
|
+
}
|
|
7459
|
+
}
|
|
7460
|
+
const selection = reconcileHyperbolicSelection(input.selected, input.pendingSelected, input.index, input.canSelect, input.singleSelect);
|
|
7461
|
+
return {
|
|
7462
|
+
center,
|
|
7463
|
+
pendingCenter,
|
|
7464
|
+
selection
|
|
7465
|
+
};
|
|
7466
|
+
}
|
|
7467
|
+
/** Sanitise a JSON zoom: a finite positive number, else `undefined`. */
|
|
7468
|
+
function zoomFromJson(value) {
|
|
7469
|
+
return typeof value === `number` && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
7470
|
+
}
|
|
7471
|
+
//#endregion
|
|
7472
|
+
//#region src/hyperbolic-tree/hyperbolic-view-state-binding.ts
|
|
7473
|
+
function createHyperbolicViewState(host) {
|
|
7474
|
+
const { controller } = host;
|
|
7475
|
+
/** Key path of the centre as last seen — what goes pending if it leaves the tree. */
|
|
7476
|
+
let centerPath = [];
|
|
7477
|
+
let pendingCenter;
|
|
7478
|
+
let pendingSelected = EMPTY_KEYS;
|
|
7479
|
+
let pendingFocus;
|
|
7480
|
+
/** Where keyboard / search navigation last put the cursor. */
|
|
7481
|
+
let keyboardCursor;
|
|
7482
|
+
let structureVersion = 0;
|
|
7483
|
+
let reconciledVersion = -1;
|
|
7484
|
+
let cache;
|
|
7485
|
+
const index = () => {
|
|
7486
|
+
if (cache?.version !== structureVersion) cache = {
|
|
7487
|
+
version: structureVersion,
|
|
7488
|
+
index: indexHyperbolicKeys(host.root())
|
|
7489
|
+
};
|
|
7490
|
+
return cache.index;
|
|
7491
|
+
};
|
|
7492
|
+
/** Key of the keyboard cursor, while the cursor is still where the keyboard put it. */
|
|
7493
|
+
const cursorKey = () => keyboardCursor && keyboardCursor.id === host.tickled() ? keyboardCursor.key : void 0;
|
|
7494
|
+
const applyCenter = (id, path, restored) => {
|
|
7495
|
+
centerPath = path;
|
|
7496
|
+
if (host.focusId() === id) return;
|
|
7497
|
+
host.setCenter(id, restored);
|
|
7498
|
+
controller.notify();
|
|
7499
|
+
};
|
|
7500
|
+
const reconcileTickled = (idx) => {
|
|
7501
|
+
const tickled = host.tickled();
|
|
7502
|
+
const alive = tickled !== void 0 && findHyperbolicPathById(host.root(), tickled) !== void 0;
|
|
7503
|
+
const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => alive && idx.byKey.has(key), (key) => host.visibleIdOf(key) !== void 0);
|
|
7504
|
+
pendingFocus = cursor.pending;
|
|
7505
|
+
if (cursor.clear || tickled !== void 0 && !alive) host.setTickled(void 0);
|
|
7506
|
+
const id = cursor.apply === void 0 ? void 0 : host.visibleIdOf(cursor.apply);
|
|
7507
|
+
if (cursor.apply !== void 0 && id !== void 0) {
|
|
7508
|
+
host.setTickled(id);
|
|
7509
|
+
keyboardCursor = {
|
|
7510
|
+
id,
|
|
7511
|
+
key: cursor.apply
|
|
7512
|
+
};
|
|
7513
|
+
controller.notify();
|
|
7514
|
+
}
|
|
7515
|
+
};
|
|
7516
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
7517
|
+
const reconcile = () => {
|
|
7518
|
+
reconciledVersion = structureVersion;
|
|
7519
|
+
const root = host.root();
|
|
7520
|
+
if (!root) return;
|
|
7521
|
+
const idx = index();
|
|
7522
|
+
controller.restoring(() => {
|
|
7523
|
+
const r = reconcileHyperbolicViewState({
|
|
7524
|
+
root,
|
|
7525
|
+
index: idx,
|
|
7526
|
+
focusId: host.focusId(),
|
|
7527
|
+
centerPath,
|
|
7528
|
+
pendingCenter,
|
|
7529
|
+
selected: host.selected(),
|
|
7530
|
+
pendingSelected,
|
|
7531
|
+
canSelect: host.canSelect,
|
|
7532
|
+
singleSelect: host.singleSelect()
|
|
7533
|
+
});
|
|
7534
|
+
pendingCenter = r.pendingCenter;
|
|
7535
|
+
if (r.center) applyCenter(r.center.id, r.center.path, r.center.restored);
|
|
7536
|
+
const sel = r.selection;
|
|
7537
|
+
if (sel.changed) {
|
|
7538
|
+
pendingSelected = sel.pending;
|
|
7539
|
+
const primary = host.selectedNode();
|
|
7540
|
+
host.setSelection(sel.selected, primary && sel.selected.has(primary) ? primary : [...sel.selected].at(-1), sel.restored);
|
|
7541
|
+
}
|
|
7542
|
+
reconcileTickled(idx);
|
|
7543
|
+
});
|
|
7544
|
+
};
|
|
7545
|
+
return {
|
|
7546
|
+
/** The structure changed (new root / model, invalidation, lazy load). */
|
|
7547
|
+
invalidate() {
|
|
7548
|
+
structureVersion++;
|
|
7549
|
+
},
|
|
7550
|
+
/** Run in `willUpdate`. `centerGone` — the centre vanished through in-place mutation. */
|
|
7551
|
+
reconcileIfNeeded(centerGone) {
|
|
7552
|
+
if (centerGone) structureVersion++;
|
|
7553
|
+
if (reconciledVersion !== structureVersion) reconcile();
|
|
7554
|
+
},
|
|
7555
|
+
/** An explicit centre change: supersedes a saved one still waiting. */
|
|
7556
|
+
centred(id) {
|
|
7557
|
+
pendingCenter = void 0;
|
|
7558
|
+
centerPath = centerPathOf(host.root(), id);
|
|
7559
|
+
controller.notify();
|
|
7560
|
+
},
|
|
7561
|
+
/** Selection changed — explicit changes supersede a saved selection still waiting. */
|
|
7562
|
+
selectionChanged() {
|
|
7563
|
+
if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
|
|
7564
|
+
controller.notify();
|
|
7565
|
+
},
|
|
7566
|
+
clearPendingSelection() {
|
|
7567
|
+
pendingSelected = EMPTY_KEYS;
|
|
7568
|
+
},
|
|
7569
|
+
/** The cursor moved by keyboard / search navigation. */
|
|
7570
|
+
keyboardCursorMoved(id, key) {
|
|
7571
|
+
keyboardCursor = {
|
|
7572
|
+
id,
|
|
7573
|
+
key
|
|
7574
|
+
};
|
|
7575
|
+
pendingFocus = void 0;
|
|
7576
|
+
controller.notify();
|
|
7577
|
+
},
|
|
7578
|
+
/** A user-visible change with no pending facet (e.g. a zoom gesture). */
|
|
7579
|
+
notify() {
|
|
7580
|
+
controller.notify();
|
|
7581
|
+
},
|
|
7582
|
+
get(options) {
|
|
7583
|
+
const withPending = options?.includePending ?? true;
|
|
7584
|
+
return controller.shape({
|
|
7585
|
+
selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
|
|
7586
|
+
focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
|
|
7587
|
+
center: [...(withPending ? pendingCenter : void 0) ?? centerPath],
|
|
7588
|
+
zoom: host.zoom()
|
|
7589
|
+
}, options);
|
|
7590
|
+
},
|
|
7591
|
+
set(state) {
|
|
7592
|
+
if (!controller.accepts(state)) return;
|
|
7593
|
+
controller.restoring(() => {
|
|
7594
|
+
if (state.selected) {
|
|
7595
|
+
pendingSelected = capKeys(new Set(state.selected));
|
|
7596
|
+
if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
|
|
7597
|
+
}
|
|
7598
|
+
if (state.focus !== void 0) pendingFocus = state.focus;
|
|
7599
|
+
const zoom = zoomFromJson(state.zoom);
|
|
7600
|
+
if (zoom !== void 0 && zoom !== host.zoom()) {
|
|
7601
|
+
host.setZoom(zoom);
|
|
7602
|
+
controller.notify();
|
|
7603
|
+
}
|
|
7604
|
+
const center = pathFromJson(state.center);
|
|
7605
|
+
if (center) pendingCenter = center;
|
|
7606
|
+
reconcile();
|
|
7607
|
+
if (center && pendingCenter) applyCenter(void 0, [], true);
|
|
7608
|
+
});
|
|
7609
|
+
},
|
|
7610
|
+
clearPending() {
|
|
7611
|
+
pendingSelected = EMPTY_KEYS;
|
|
7612
|
+
pendingFocus = void 0;
|
|
7613
|
+
pendingCenter = void 0;
|
|
7614
|
+
},
|
|
7615
|
+
/** Load the next unloaded ancestor on a pending centre path (one load at a time). */
|
|
7616
|
+
continueRestoreLoads() {
|
|
7617
|
+
if (!pendingCenter || !host.restoreLoads()) return;
|
|
7618
|
+
const resolved = resolveCenterPath(host.root(), pendingCenter);
|
|
7619
|
+
if (resolved.kind === `load`) host.load(resolved.node, resolved.depth);
|
|
7620
|
+
}
|
|
7621
|
+
};
|
|
7622
|
+
}
|
|
7623
|
+
//#endregion
|
|
7624
|
+
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
7625
|
+
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
7626
|
+
const hyperbolicTreeStyles = css`
|
|
7627
|
+
:host {
|
|
7628
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
7629
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
7630
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
7631
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
7632
|
+
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
7633
|
+
--hyperbolictree-font-size: 13px;
|
|
7634
|
+
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
7635
|
+
--hyperbolictree-box-text: var(--surface-4-text);
|
|
7636
|
+
--hyperbolictree-box-border: var(--surface-2);
|
|
7637
|
+
--hyperbolictree-branch-hue-start: 250;
|
|
7638
|
+
--hyperbolictree-branch-chroma: 0.13;
|
|
7639
|
+
--hyperbolictree-branch-lightness: 62%;
|
|
7640
|
+
--hyperbolictree-branch-mix: 55%;
|
|
7641
|
+
|
|
7642
|
+
display: block;
|
|
7643
|
+
position: relative;
|
|
7644
|
+
overflow: hidden;
|
|
7645
|
+
outline: none;
|
|
7646
|
+
overscroll-behavior: contain;
|
|
7647
|
+
}
|
|
7648
|
+
|
|
7649
|
+
/*
|
|
7650
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
7651
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
7652
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
7653
|
+
* sized to the whole box.
|
|
7654
|
+
*/
|
|
7655
|
+
:host(:focus),
|
|
7656
|
+
:host(:focus-visible) {
|
|
7657
|
+
outline: none;
|
|
7658
|
+
}
|
|
7659
|
+
|
|
7660
|
+
.hyperbolictree {
|
|
7661
|
+
display: block;
|
|
7662
|
+
width: 100%;
|
|
7663
|
+
height: 100%;
|
|
7664
|
+
outline: none;
|
|
7665
|
+
touch-action: none;
|
|
7666
|
+
overscroll-behavior: contain;
|
|
7667
|
+
}
|
|
7668
|
+
|
|
7669
|
+
.disk-boundary {
|
|
7670
|
+
fill: none;
|
|
7671
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
7672
|
+
stroke-width: 1;
|
|
7673
|
+
}
|
|
7674
|
+
|
|
7675
|
+
.edge {
|
|
7676
|
+
fill: none;
|
|
7677
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
7678
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
7679
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
7680
|
+
/* Constant on-screen width, however far the disk is zoomed. */
|
|
7681
|
+
vector-effect: non-scaling-stroke;
|
|
7682
|
+
}
|
|
7683
|
+
|
|
7684
|
+
.node {
|
|
7685
|
+
cursor: pointer;
|
|
7686
|
+
}
|
|
7687
|
+
|
|
7688
|
+
/*
|
|
7689
|
+
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
7690
|
+
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
7691
|
+
* so it stays in keeping with — and legible in — the current theme.
|
|
7692
|
+
* Only sets the base colour vars, so selected/tickled states still win.
|
|
7693
|
+
*/
|
|
7694
|
+
:host([color-by-branch]) .node,
|
|
7695
|
+
:host([color-by-branch]) .edge {
|
|
7696
|
+
--_branch-color: oklch(
|
|
7697
|
+
var(--hyperbolictree-branch-lightness)
|
|
7698
|
+
var(--hyperbolictree-branch-chroma)
|
|
7699
|
+
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
7700
|
+
);
|
|
7701
|
+
}
|
|
7702
|
+
|
|
7703
|
+
:host([color-by-branch]) .node {
|
|
7704
|
+
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
7705
|
+
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
7706
|
+
}
|
|
7707
|
+
|
|
7708
|
+
:host([color-by-branch]) .edge {
|
|
7709
|
+
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
7710
|
+
}
|
|
7711
|
+
|
|
7712
|
+
.node-hit {
|
|
7713
|
+
fill: transparent;
|
|
7714
|
+
pointer-events: all;
|
|
7715
|
+
}
|
|
7716
|
+
|
|
7717
|
+
.node-dot {
|
|
7718
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
7719
|
+
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
7720
|
+
stroke: var(--surface-2);
|
|
7721
|
+
stroke-width: 1;
|
|
7722
|
+
pointer-events: none;
|
|
7723
|
+
}
|
|
7724
|
+
|
|
7725
|
+
.node-box {
|
|
7726
|
+
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
7727
|
+
stroke: var(--hyperbolictree-box-border);
|
|
7728
|
+
stroke-width: 1.5;
|
|
7729
|
+
/* Keep the border crisp however far the box has been scaled down. */
|
|
7730
|
+
vector-effect: non-scaling-stroke;
|
|
7731
|
+
}
|
|
7732
|
+
|
|
7733
|
+
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
7734
|
+
.node-box.marker {
|
|
7735
|
+
stroke-width: 1;
|
|
7736
|
+
}
|
|
7737
|
+
|
|
7738
|
+
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
7739
|
+
.hyperbolictree-label {
|
|
7740
|
+
fill: var(--hyperbolictree-node-text);
|
|
7741
|
+
cursor: pointer;
|
|
7742
|
+
user-select: none;
|
|
7743
|
+
}
|
|
7744
|
+
|
|
7745
|
+
.box-label {
|
|
7746
|
+
fill: var(--hyperbolictree-box-text);
|
|
7747
|
+
pointer-events: none;
|
|
7748
|
+
}
|
|
7749
|
+
|
|
7750
|
+
/*
|
|
7751
|
+
* State fills come paired with their theme text tokens: a box label sits
|
|
7752
|
+
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
7753
|
+
* --item-text-selected on top, not the resting box text colour.
|
|
7754
|
+
*/
|
|
7755
|
+
.node.tickled .node-dot,
|
|
7756
|
+
.node.tickled .node-box {
|
|
7757
|
+
fill: var(--item-bg-tickled);
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
.node.tickled .box-label {
|
|
7761
|
+
fill: var(--item-text-tickled);
|
|
7762
|
+
}
|
|
7763
|
+
|
|
7764
|
+
.node.selected .node-dot,
|
|
7765
|
+
.node.selected .node-box {
|
|
7766
|
+
fill: var(--item-bg-selected);
|
|
7767
|
+
}
|
|
7768
|
+
|
|
7769
|
+
.node.selected .box-label {
|
|
7770
|
+
fill: var(--item-text-selected);
|
|
7771
|
+
}
|
|
7772
|
+
|
|
7773
|
+
.node.selected.tickled .node-dot,
|
|
7774
|
+
.node.selected.tickled .node-box {
|
|
7775
|
+
fill: var(--item-bg-selected-tickled);
|
|
7776
|
+
}
|
|
7777
|
+
|
|
7778
|
+
.node.focused .node-dot,
|
|
7779
|
+
.node.focused .node-box {
|
|
7780
|
+
stroke: var(--accent);
|
|
7781
|
+
stroke-width: 3;
|
|
7782
|
+
}
|
|
7783
|
+
|
|
7784
|
+
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
7785
|
+
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
7786
|
+
paint-order: stroke;
|
|
7787
|
+
stroke: var(--surface-2);
|
|
7788
|
+
stroke-width: 4px;
|
|
7789
|
+
stroke-linejoin: round;
|
|
7790
|
+
}
|
|
7791
|
+
|
|
7792
|
+
.node.filtered {
|
|
7793
|
+
opacity: 0.3;
|
|
7794
|
+
}
|
|
7795
|
+
|
|
7796
|
+
.node.loading .node-dot,
|
|
7797
|
+
.node.loading .node-box {
|
|
7798
|
+
opacity: 0.6;
|
|
7799
|
+
}
|
|
7800
|
+
|
|
7801
|
+
|
|
7802
|
+
/* Child preview: a clickable list of a crowded node's children. */
|
|
7803
|
+
.child-preview {
|
|
7804
|
+
position: absolute;
|
|
7805
|
+
z-index: 1;
|
|
7806
|
+
min-width: 8rem;
|
|
7807
|
+
max-width: min(18rem, 60%);
|
|
7808
|
+
max-height: 45%;
|
|
7809
|
+
overflow: auto;
|
|
7810
|
+
padding: var(--space-xs, 4px);
|
|
7811
|
+
background: var(--surface-3);
|
|
7812
|
+
color: var(--surface-3-text);
|
|
7813
|
+
border: var(--border-subtle);
|
|
7814
|
+
border-radius: var(--radius-m, 6px);
|
|
7815
|
+
box-shadow: var(--shadow-m);
|
|
7816
|
+
font-size: var(--hyperbolictree-font-size);
|
|
7817
|
+
/* Beside the node, never over it, with the dot level with the popover's middle. */
|
|
7818
|
+
transform: translate(14px, -50%);
|
|
7819
|
+
}
|
|
7820
|
+
|
|
7821
|
+
.child-preview.left {
|
|
7822
|
+
transform: translate(calc(-100% - 14px), -50%);
|
|
7823
|
+
}
|
|
7824
|
+
|
|
7825
|
+
.child-preview.below {
|
|
7826
|
+
transform: translate(-50%, 14px);
|
|
7827
|
+
}
|
|
7828
|
+
|
|
7829
|
+
.child-preview.above {
|
|
7830
|
+
transform: translate(-50%, calc(-100% - 14px));
|
|
7831
|
+
}
|
|
7832
|
+
|
|
7833
|
+
.child-preview-title {
|
|
7834
|
+
display: flex;
|
|
7835
|
+
align-items: center;
|
|
7836
|
+
gap: 4px;
|
|
7837
|
+
padding: 2px 6px 4px;
|
|
7838
|
+
font-weight: 600;
|
|
7839
|
+
}
|
|
7840
|
+
|
|
7841
|
+
.child-preview-title span {
|
|
7842
|
+
overflow: hidden;
|
|
7843
|
+
text-overflow: ellipsis;
|
|
7844
|
+
white-space: nowrap;
|
|
7845
|
+
}
|
|
7846
|
+
|
|
7847
|
+
.child-preview button.child-preview-up {
|
|
7848
|
+
display: inline-flex;
|
|
7849
|
+
flex: none;
|
|
7850
|
+
width: auto;
|
|
7851
|
+
padding: 2px;
|
|
7852
|
+
}
|
|
7853
|
+
|
|
7854
|
+
.child-preview ul {
|
|
7855
|
+
margin: 0;
|
|
7856
|
+
padding: 0;
|
|
7857
|
+
list-style: none;
|
|
7858
|
+
}
|
|
7859
|
+
|
|
7860
|
+
.child-preview button {
|
|
7861
|
+
display: block;
|
|
7862
|
+
width: 100%;
|
|
7863
|
+
padding: 2px 6px;
|
|
7864
|
+
border: none;
|
|
7865
|
+
border-radius: var(--radius-s, 4px);
|
|
7866
|
+
background: none;
|
|
7867
|
+
color: inherit;
|
|
7868
|
+
font: inherit;
|
|
7869
|
+
text-align: start;
|
|
7870
|
+
overflow: hidden;
|
|
7871
|
+
text-overflow: ellipsis;
|
|
7872
|
+
white-space: nowrap;
|
|
7873
|
+
cursor: pointer;
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
.child-preview button:hover {
|
|
7877
|
+
background: var(--item-bg-tickled);
|
|
7878
|
+
color: var(--item-text-tickled);
|
|
7879
|
+
}
|
|
7880
|
+
|
|
7881
|
+
.child-preview-more {
|
|
7882
|
+
padding: 2px 6px;
|
|
7883
|
+
color: var(--surface-muted-text);
|
|
7884
|
+
}
|
|
7885
|
+
`;
|
|
7886
|
+
//#endregion
|
|
7447
7887
|
//#region src/hyperbolic-tree/keyboard.ts
|
|
7448
7888
|
const unhandled = {
|
|
7449
7889
|
action: { kind: `none` },
|
|
@@ -7542,6 +7982,11 @@ function createLazyChildLoader(source, onChange) {
|
|
|
7542
7982
|
if (!model && !loadChildren) return;
|
|
7543
7983
|
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
7544
7984
|
},
|
|
7985
|
+
/** Start loading `node`'s children unless loaded or already loading (view-state restore). */
|
|
7986
|
+
loadOne(node, depth) {
|
|
7987
|
+
const { model, loadChildren } = source();
|
|
7988
|
+
if ((model || loadChildren) && node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
7989
|
+
},
|
|
7545
7990
|
isPending: (node) => pending.has(node),
|
|
7546
7991
|
abortAll() {
|
|
7547
7992
|
for (const aborter of aborters.values()) aborter.abort();
|
|
@@ -8062,6 +8507,35 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8062
8507
|
this.disableKeyboardNavigation = false;
|
|
8063
8508
|
this.registry = void 0;
|
|
8064
8509
|
this._focusId = void 0;
|
|
8510
|
+
this.restoreLoads = true;
|
|
8511
|
+
this.#viewState = createHyperbolicViewState({
|
|
8512
|
+
controller: new ViewStateController(this, {
|
|
8513
|
+
kind: `hyperbolictree-list`,
|
|
8514
|
+
capture: () => this.getViewState()
|
|
8515
|
+
}),
|
|
8516
|
+
root: () => this.#effectiveRoot,
|
|
8517
|
+
focusId: () => this._focusId,
|
|
8518
|
+
setCenter: (id, restored) => this.#restoreCenter(id, restored),
|
|
8519
|
+
selected: () => this._selectedNodes,
|
|
8520
|
+
selectedNode: () => this.selectedNode,
|
|
8521
|
+
canSelect: (node) => this.#canSelect(node),
|
|
8522
|
+
singleSelect: () => this.interactionMode === `implicit`,
|
|
8523
|
+
setSelection: (next, primary, fire) => {
|
|
8524
|
+
const prev = this._selectedNodes;
|
|
8525
|
+
this._selectedNodes = new Set(next);
|
|
8526
|
+
this.selectedNode = primary;
|
|
8527
|
+
if (fire) this.#dispatchSelect(prev);
|
|
8528
|
+
},
|
|
8529
|
+
tickled: () => this.tickled.tickledId,
|
|
8530
|
+
setTickled: (id) => id === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(id),
|
|
8531
|
+
visibleIdOf: (key) => this.#layout().nodes.find((n) => n.key === key)?.id,
|
|
8532
|
+
zoom: () => this.zoom,
|
|
8533
|
+
setZoom: (zoom) => {
|
|
8534
|
+
this.zoom = zoom;
|
|
8535
|
+
},
|
|
8536
|
+
restoreLoads: () => this.restoreLoads,
|
|
8537
|
+
load: (node, depth) => this.#lazy.loadOne(node, depth)
|
|
8538
|
+
});
|
|
8065
8539
|
this._viewOverride = void 0;
|
|
8066
8540
|
this._focusRotation = 0;
|
|
8067
8541
|
this._zoomOffset = {
|
|
@@ -8083,7 +8557,10 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8083
8557
|
this.#lazy = createLazyChildLoader(() => ({
|
|
8084
8558
|
model: this.model,
|
|
8085
8559
|
loadChildren: this.loadChildren
|
|
8086
|
-
}), () =>
|
|
8560
|
+
}), () => {
|
|
8561
|
+
this.#viewState.invalidate();
|
|
8562
|
+
this.requestUpdate();
|
|
8563
|
+
});
|
|
8087
8564
|
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
8088
8565
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8089
8566
|
labelSelector: `.hyperbolictree-label`,
|
|
@@ -8130,6 +8607,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8130
8607
|
},
|
|
8131
8608
|
zoomState: () => this.#zoomState(),
|
|
8132
8609
|
setZoomState: (state) => {
|
|
8610
|
+
if (state.zoom !== this.zoom) this.#viewState.notify();
|
|
8133
8611
|
this.zoom = state.zoom;
|
|
8134
8612
|
this._zoomOffset = state.offset;
|
|
8135
8613
|
},
|
|
@@ -8152,6 +8630,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8152
8630
|
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
8153
8631
|
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
8154
8632
|
}
|
|
8633
|
+
#viewState;
|
|
8155
8634
|
#preview;
|
|
8156
8635
|
#lazy;
|
|
8157
8636
|
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
@@ -8259,46 +8738,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8259
8738
|
this.#unregisterSearchCommands?.();
|
|
8260
8739
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
8261
8740
|
}
|
|
8741
|
+
willUpdate(changedProps) {
|
|
8742
|
+
if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
|
|
8743
|
+
this.#viewState.reconcileIfNeeded(this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId));
|
|
8744
|
+
}
|
|
8262
8745
|
updated(changedProps) {
|
|
8263
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
8264
8746
|
if (changedProps.has(`model`)) {
|
|
8265
8747
|
this.#modelUnsubscribe?.();
|
|
8266
8748
|
this.#modelUnsubscribe = void 0;
|
|
8267
|
-
if (this.model) {
|
|
8268
|
-
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
8273
|
-
});
|
|
8274
|
-
this.#resetViewState();
|
|
8275
|
-
}
|
|
8749
|
+
if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
8750
|
+
if (event.type === `root`) this.#onDataReset();
|
|
8751
|
+
else if (event.type === `invalidate`) this.#viewState.invalidate();
|
|
8752
|
+
this.requestUpdate();
|
|
8753
|
+
});
|
|
8276
8754
|
}
|
|
8277
|
-
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
8755
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) this.clearSelection();
|
|
8278
8756
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
8279
|
-
if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
|
|
8280
|
-
this._focusId = void 0;
|
|
8281
|
-
this._viewOverride = void 0;
|
|
8282
|
-
this._focusRotation = 0;
|
|
8283
|
-
}
|
|
8284
8757
|
this.#lazy.ensure(this.#layout().nodes);
|
|
8758
|
+
this.#viewState.continueRestoreLoads();
|
|
8285
8759
|
}
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
this.tickled.clearTickled();
|
|
8289
|
-
this._focusId = void 0;
|
|
8290
|
-
this._viewOverride = void 0;
|
|
8291
|
-
this._focusRotation = 0;
|
|
8292
|
-
this._zoomOffset = {
|
|
8293
|
-
x: 0,
|
|
8294
|
-
y: 0
|
|
8295
|
-
};
|
|
8760
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
8761
|
+
#onDataReset() {
|
|
8296
8762
|
this._previewId = void 0;
|
|
8297
8763
|
this.#gestures.cancel();
|
|
8298
|
-
|
|
8299
|
-
cancelAnimationFrame(this.#animFrame);
|
|
8300
|
-
this.#animFrame = void 0;
|
|
8301
|
-
}
|
|
8764
|
+
this.#viewState.invalidate();
|
|
8302
8765
|
}
|
|
8303
8766
|
#canSelect(node) {
|
|
8304
8767
|
if (this.selectionFilter === `none`) return false;
|
|
@@ -8311,6 +8774,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8311
8774
|
this.selectedNode = void 0;
|
|
8312
8775
|
}
|
|
8313
8776
|
#dispatchSelect(previous) {
|
|
8777
|
+
this.#viewState.selectionChanged();
|
|
8314
8778
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
8315
8779
|
detail: {
|
|
8316
8780
|
selected: new Set(this._selectedNodes),
|
|
@@ -8338,6 +8802,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8338
8802
|
this.requestUpdate();
|
|
8339
8803
|
}
|
|
8340
8804
|
clearSelection() {
|
|
8805
|
+
this.#viewState.clearPendingSelection();
|
|
8341
8806
|
if (this._selectedNodes.size === 0) return;
|
|
8342
8807
|
const prev = new Set(this._selectedNodes);
|
|
8343
8808
|
this.#clearSelectionInternal();
|
|
@@ -8383,6 +8848,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8383
8848
|
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
8384
8849
|
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
8385
8850
|
this._focusId = targetId;
|
|
8851
|
+
this.#viewState.centred(targetId);
|
|
8386
8852
|
this.#gestures.cancel();
|
|
8387
8853
|
this._previewId = void 0;
|
|
8388
8854
|
this.#animateTo(fromView);
|
|
@@ -8451,7 +8917,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8451
8917
|
#focusAndMaybeSelect(node) {
|
|
8452
8918
|
this.focusOn(node);
|
|
8453
8919
|
const id = this.#idOf(node);
|
|
8454
|
-
if (id !== void 0) this
|
|
8920
|
+
if (id !== void 0) this.#setKeyboardCursor(id, node);
|
|
8455
8921
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
8456
8922
|
}
|
|
8457
8923
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -8489,10 +8955,15 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8489
8955
|
const id = node ? this.#idOf(node) : void 0;
|
|
8490
8956
|
if (!node || id === void 0) return false;
|
|
8491
8957
|
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
8492
|
-
this
|
|
8958
|
+
this.#setKeyboardCursor(id, node);
|
|
8493
8959
|
this.requestUpdate();
|
|
8494
8960
|
return true;
|
|
8495
8961
|
}
|
|
8962
|
+
/** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
|
|
8963
|
+
#setKeyboardCursor(id, node) {
|
|
8964
|
+
this.tickled.setTickled(id);
|
|
8965
|
+
this.#viewState.keyboardCursorMoved(id, node.item.key);
|
|
8966
|
+
}
|
|
8496
8967
|
/**
|
|
8497
8968
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
8498
8969
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -8505,6 +8976,49 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8505
8976
|
depth: n.depth
|
|
8506
8977
|
}));
|
|
8507
8978
|
}
|
|
8979
|
+
/**
|
|
8980
|
+
* Snapshot selection, centred node, keyboard cursor and zoom as plain JSON,
|
|
8981
|
+
* keyed by `item.key`. Includes pending state for keys not currently loaded.
|
|
8982
|
+
*/
|
|
8983
|
+
getViewState(options) {
|
|
8984
|
+
return this.#viewState.get(options);
|
|
8985
|
+
}
|
|
8986
|
+
/**
|
|
8987
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
8988
|
+
* that aren't loaded yet are applied as they appear — a deep `center` loads
|
|
8989
|
+
* its ancestors via `loadChildren` / `model` when `restoreLoads`.
|
|
8990
|
+
*/
|
|
8991
|
+
setViewState(state) {
|
|
8992
|
+
this.#viewState.set(state);
|
|
8993
|
+
this.requestUpdate();
|
|
8994
|
+
}
|
|
8995
|
+
clearPendingViewState() {
|
|
8996
|
+
this.#viewState.clearPending();
|
|
8997
|
+
}
|
|
8998
|
+
/** Centre set by view-state reconcile: snaps (no animation); `restored` fires `focus`. */
|
|
8999
|
+
#restoreCenter(id, restored) {
|
|
9000
|
+
const previous = this.focusedNode;
|
|
9001
|
+
this.#cancelAnimation();
|
|
9002
|
+
this._focusId = id;
|
|
9003
|
+
this._viewOverride = void 0;
|
|
9004
|
+
this._zoomOffset = {
|
|
9005
|
+
x: 0,
|
|
9006
|
+
y: 0
|
|
9007
|
+
};
|
|
9008
|
+
if (!restored) {
|
|
9009
|
+
this._focusRotation = 0;
|
|
9010
|
+
return;
|
|
9011
|
+
}
|
|
9012
|
+
const node = this.focusedNode;
|
|
9013
|
+
if (node) this.dispatchEvent(new CustomEvent(`focus`, {
|
|
9014
|
+
detail: {
|
|
9015
|
+
node,
|
|
9016
|
+
previous
|
|
9017
|
+
},
|
|
9018
|
+
bubbles: true,
|
|
9019
|
+
composed: true
|
|
9020
|
+
}));
|
|
9021
|
+
}
|
|
8508
9022
|
#matchesFilter(node, depth) {
|
|
8509
9023
|
if (!this.filterPredicate) return true;
|
|
8510
9024
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -8790,6 +9304,7 @@ __decorate([property({
|
|
|
8790
9304
|
reflect: true
|
|
8791
9305
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
8792
9306
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
9307
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "restoreLoads", void 0);
|
|
8793
9308
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
8794
9309
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
8795
9310
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
@@ -8824,6 +9339,30 @@ function findIciclePath(root, key) {
|
|
|
8824
9339
|
}
|
|
8825
9340
|
}
|
|
8826
9341
|
/**
|
|
9342
|
+
* Follow `keys` from `root`; a leading key equal to the root's own key
|
|
9343
|
+
* matches the root itself. Returns `undefined` when a key isn't found.
|
|
9344
|
+
*/
|
|
9345
|
+
function resolveIcicleKeyPath(root, keys) {
|
|
9346
|
+
let current = root;
|
|
9347
|
+
for (const key of keys) {
|
|
9348
|
+
const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
|
|
9349
|
+
if (!found) return void 0;
|
|
9350
|
+
current = found;
|
|
9351
|
+
}
|
|
9352
|
+
return current;
|
|
9353
|
+
}
|
|
9354
|
+
/** Key path (depth-first) to the first node among `nodes` matching `predicate`. */
|
|
9355
|
+
function findIcicleKeyPath(nodes, predicate, prefix = []) {
|
|
9356
|
+
for (const node of nodes) {
|
|
9357
|
+
const path = [...prefix, node.item.key];
|
|
9358
|
+
if (predicate(node)) return path;
|
|
9359
|
+
if (node.children?.length) {
|
|
9360
|
+
const hit = findIcicleKeyPath(node.children, predicate, path);
|
|
9361
|
+
if (hit) return hit;
|
|
9362
|
+
}
|
|
9363
|
+
}
|
|
9364
|
+
}
|
|
9365
|
+
/**
|
|
8827
9366
|
* Compute the visible rectangles for an icicle view.
|
|
8828
9367
|
*
|
|
8829
9368
|
* Bands start at the zoom node's **children** (the zoom node itself is shown in
|
|
@@ -8877,6 +9416,259 @@ function computeIcicleLayout(root, options = {}) {
|
|
|
8877
9416
|
return rects;
|
|
8878
9417
|
}
|
|
8879
9418
|
//#endregion
|
|
9419
|
+
//#region src/icicle-tree/icicle-view-state.ts
|
|
9420
|
+
/**
|
|
9421
|
+
* - `found` — every key matched: `nodes[i]` is the node for `keys[i]`.
|
|
9422
|
+
* - `load` — `node` (at `depth`; root's children are `0`) must load its
|
|
9423
|
+
* children before the path can continue.
|
|
9424
|
+
* - `missing` — a key isn't there (yet), or the next step can't load.
|
|
9425
|
+
*/
|
|
9426
|
+
/** First node (depth-first) for each key in the loaded tree. */
|
|
9427
|
+
function indexNodesByKey(root) {
|
|
9428
|
+
const out = /* @__PURE__ */ new Map();
|
|
9429
|
+
const visit = (node) => {
|
|
9430
|
+
if (!out.has(node.item.key)) out.set(node.item.key, node);
|
|
9431
|
+
for (const child of node.children ?? []) visit(child);
|
|
9432
|
+
};
|
|
9433
|
+
if (root) visit(root);
|
|
9434
|
+
return out;
|
|
9435
|
+
}
|
|
9436
|
+
/**
|
|
9437
|
+
* Nodes along a pending zoom path (`[]` = full tree), or `undefined` while it
|
|
9438
|
+
* can't resolve. Keys are unique tree-wide, so a zoom node that has moved is
|
|
9439
|
+
* still found by its key.
|
|
9440
|
+
*/
|
|
9441
|
+
function resolveZoomPath$1(root, path, nodeByKey) {
|
|
9442
|
+
const found = resolveKeyPath(root, path, () => false);
|
|
9443
|
+
if (found.status === `found`) return found.nodes;
|
|
9444
|
+
const key = path.at(-1);
|
|
9445
|
+
return key !== void 0 && nodeByKey(key) ? findIciclePath(root, key)?.slice(1) : void 0;
|
|
9446
|
+
}
|
|
9447
|
+
/** Pixel scroll of a scrolling host. Rects aren't a linear list, so there's no anchor. */
|
|
9448
|
+
function captureHostScroll(el) {
|
|
9449
|
+
return el.scrollLeft === 0 ? { top: el.scrollTop } : {
|
|
9450
|
+
top: el.scrollTop,
|
|
9451
|
+
left: el.scrollLeft
|
|
9452
|
+
};
|
|
9453
|
+
}
|
|
9454
|
+
/** Returns `true` once the saved position is fully reached (else retry after the next render). */
|
|
9455
|
+
function applyHostScroll(el, scroll) {
|
|
9456
|
+
const top = scrollToOffset(el, scroll.top);
|
|
9457
|
+
const left = scroll.left === void 0 || scrollToOffset(el, scroll.left, `horizontal`);
|
|
9458
|
+
return top && left;
|
|
9459
|
+
}
|
|
9460
|
+
//#endregion
|
|
9461
|
+
//#region src/icicle-tree/keyboard.ts
|
|
9462
|
+
/**
|
|
9463
|
+
* Rect the cursor moves to for `key`, or `undefined` when the key isn't a
|
|
9464
|
+
* navigation key or there's nowhere to go. With no current rect, any
|
|
9465
|
+
* navigation key lands on the first rect.
|
|
9466
|
+
*/
|
|
9467
|
+
function icicleKeyTarget(rects, current, key) {
|
|
9468
|
+
if (rects.length === 0) return void 0;
|
|
9469
|
+
if (key === `Home`) return rects[0];
|
|
9470
|
+
if (key === `End`) return rects.at(-1);
|
|
9471
|
+
if (![
|
|
9472
|
+
`ArrowLeft`,
|
|
9473
|
+
`ArrowRight`,
|
|
9474
|
+
`ArrowUp`,
|
|
9475
|
+
`ArrowDown`
|
|
9476
|
+
].includes(key)) return void 0;
|
|
9477
|
+
if (!current) return rects[0];
|
|
9478
|
+
switch (key) {
|
|
9479
|
+
case `ArrowLeft`:
|
|
9480
|
+
case `ArrowRight`: {
|
|
9481
|
+
const dir = key === `ArrowRight` ? 1 : -1;
|
|
9482
|
+
const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
|
|
9483
|
+
return siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1));
|
|
9484
|
+
}
|
|
9485
|
+
case `ArrowUp`: return (current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0];
|
|
9486
|
+
default: return rects.find((r) => r.parentKey === current.node.item.key);
|
|
9487
|
+
}
|
|
9488
|
+
}
|
|
9489
|
+
//#endregion
|
|
9490
|
+
//#region src/icicle-tree/styles.ts
|
|
9491
|
+
/** Shadow-DOM styles for `ixfx-icicletree-list`. */
|
|
9492
|
+
const icicleTreeStyles = css`
|
|
9493
|
+
:host {
|
|
9494
|
+
--icicletree-band-height: 44px;
|
|
9495
|
+
--icicletree-band-gap: 2px;
|
|
9496
|
+
--icicletree-rect-gap: 2px;
|
|
9497
|
+
--icicletree-rect-bg: var(--surface-4);
|
|
9498
|
+
--icicletree-rect-text: var(--surface-4-text);
|
|
9499
|
+
--icicletree-font-size: var(--text-s);
|
|
9500
|
+
|
|
9501
|
+
display: block;
|
|
9502
|
+
overflow: auto;
|
|
9503
|
+
}
|
|
9504
|
+
|
|
9505
|
+
.icicle {
|
|
9506
|
+
display: flex;
|
|
9507
|
+
gap: var(--icicletree-band-gap);
|
|
9508
|
+
outline: none;
|
|
9509
|
+
}
|
|
9510
|
+
|
|
9511
|
+
.icicle.horizontal {
|
|
9512
|
+
flex-direction: column;
|
|
9513
|
+
}
|
|
9514
|
+
|
|
9515
|
+
.icicle.vertical {
|
|
9516
|
+
flex-direction: row;
|
|
9517
|
+
min-height: 200px;
|
|
9518
|
+
}
|
|
9519
|
+
|
|
9520
|
+
.breadcrumb {
|
|
9521
|
+
display: flex;
|
|
9522
|
+
align-items: center;
|
|
9523
|
+
padding: var(--space-xs) var(--space-s);
|
|
9524
|
+
background: var(--surface-3);
|
|
9525
|
+
color: var(--surface-3-text);
|
|
9526
|
+
border: var(--border-subtle);
|
|
9527
|
+
border-radius: var(--radius-m);
|
|
9528
|
+
font-size: var(--text-s);
|
|
9529
|
+
flex-shrink: 0;
|
|
9530
|
+
}
|
|
9531
|
+
|
|
9532
|
+
.breadcrumb ixfx-crumb-navigation {
|
|
9533
|
+
flex: 1;
|
|
9534
|
+
min-width: 0;
|
|
9535
|
+
}
|
|
9536
|
+
|
|
9537
|
+
.band {
|
|
9538
|
+
position: relative;
|
|
9539
|
+
}
|
|
9540
|
+
|
|
9541
|
+
.icicle.horizontal .band {
|
|
9542
|
+
height: var(--icicletree-band-height);
|
|
9543
|
+
}
|
|
9544
|
+
|
|
9545
|
+
.icicle.vertical .band {
|
|
9546
|
+
flex: 1 1 0;
|
|
9547
|
+
min-width: 0;
|
|
9548
|
+
height: 100%;
|
|
9549
|
+
}
|
|
9550
|
+
|
|
9551
|
+
.rect {
|
|
9552
|
+
box-sizing: border-box;
|
|
9553
|
+
position: absolute;
|
|
9554
|
+
overflow: hidden;
|
|
9555
|
+
display: flex;
|
|
9556
|
+
align-items: center;
|
|
9557
|
+
padding: 0 var(--space-s);
|
|
9558
|
+
background: var(--icicletree-rect-bg);
|
|
9559
|
+
color: var(--icicletree-rect-text);
|
|
9560
|
+
font-size: var(--icicletree-font-size);
|
|
9561
|
+
cursor: pointer;
|
|
9562
|
+
transition:
|
|
9563
|
+
width var(--transition),
|
|
9564
|
+
height var(--transition),
|
|
9565
|
+
left var(--transition),
|
|
9566
|
+
top var(--transition),
|
|
9567
|
+
opacity var(--transition),
|
|
9568
|
+
background-color var(--transition);
|
|
9569
|
+
}
|
|
9570
|
+
|
|
9571
|
+
.icicle.horizontal .rect {
|
|
9572
|
+
top: 0;
|
|
9573
|
+
height: 100%;
|
|
9574
|
+
}
|
|
9575
|
+
|
|
9576
|
+
/*
|
|
9577
|
+
* A rotated label needs its full allotted width to be usable — the
|
|
9578
|
+
* default left/right padding (--space-s, ~4px a side) can consume
|
|
9579
|
+
* most or all of a rect sized down to minVerticalLabelWidth, leaving
|
|
9580
|
+
* the rotated text clipped to invisible. #labelMode's width estimate
|
|
9581
|
+
* assumes the whole rect width is usable, so drop the padding here to
|
|
9582
|
+
* match that assumption instead of also budgeting for it in the
|
|
9583
|
+
* threshold (which would make already-narrow rects need even more).
|
|
9584
|
+
*/
|
|
9585
|
+
.icicle.horizontal .rect.rotated {
|
|
9586
|
+
padding: 0;
|
|
9587
|
+
}
|
|
9588
|
+
|
|
9589
|
+
.icicle.vertical .rect {
|
|
9590
|
+
left: 0;
|
|
9591
|
+
width: 100%;
|
|
9592
|
+
padding: var(--space-s) 0;
|
|
9593
|
+
justify-content: center;
|
|
9594
|
+
writing-mode: horizontal-tb;
|
|
9595
|
+
}
|
|
9596
|
+
|
|
9597
|
+
.rect.filtered {
|
|
9598
|
+
opacity: 0.3;
|
|
9599
|
+
filter: saturate(0.4);
|
|
9600
|
+
}
|
|
9601
|
+
|
|
9602
|
+
.rect.empty {
|
|
9603
|
+
padding: 0;
|
|
9604
|
+
border: none;
|
|
9605
|
+
}
|
|
9606
|
+
|
|
9607
|
+
.rect.loading {
|
|
9608
|
+
cursor: progress;
|
|
9609
|
+
opacity: 0.6;
|
|
9610
|
+
}
|
|
9611
|
+
|
|
9612
|
+
.icicle-label {
|
|
9613
|
+
overflow: hidden;
|
|
9614
|
+
text-overflow: ellipsis;
|
|
9615
|
+
white-space: nowrap;
|
|
9616
|
+
flex: 1;
|
|
9617
|
+
min-width: 0;
|
|
9618
|
+
}
|
|
9619
|
+
|
|
9620
|
+
.icicle-label.hide-label {
|
|
9621
|
+
visibility: hidden;
|
|
9622
|
+
}
|
|
9623
|
+
|
|
9624
|
+
.icicle-label.label-vertical {
|
|
9625
|
+
writing-mode: vertical-rl;
|
|
9626
|
+
text-orientation: sideways;
|
|
9627
|
+
flex: none;
|
|
9628
|
+
width: 100%;
|
|
9629
|
+
height: 100%;
|
|
9630
|
+
padding: var(--space-2xs, 2px) 0;
|
|
9631
|
+
box-sizing: border-box;
|
|
9632
|
+
display: flex;
|
|
9633
|
+
align-items: center;
|
|
9634
|
+
justify-content: center;
|
|
9635
|
+
}
|
|
9636
|
+
|
|
9637
|
+
/*
|
|
9638
|
+
* Instant tooltip for rects whose label is rotated or hidden — anchored
|
|
9639
|
+
* via CSS anchor positioning (anchor-name set on the tickled rect in
|
|
9640
|
+
* #renderRect) rather than a JS-computed position, and shown/hidden via
|
|
9641
|
+
* the Popover API rather than the browser's own (slow, un-stylable)
|
|
9642
|
+
* title tooltip.
|
|
9643
|
+
*/
|
|
9644
|
+
.icicle-tooltip {
|
|
9645
|
+
position: fixed;
|
|
9646
|
+
position-anchor: --icicle-rect-anchor;
|
|
9647
|
+
position-area: bottom center;
|
|
9648
|
+
position-try-fallbacks: top center;
|
|
9649
|
+
margin: var(--space-xs, 4px) 0 0;
|
|
9650
|
+
padding: var(--space-2xs, 2px) var(--space-xs, 6px);
|
|
9651
|
+
background: var(--surface-5);
|
|
9652
|
+
color: var(--surface-5-text);
|
|
9653
|
+
border: var(--border-subtle);
|
|
9654
|
+
border-radius: var(--radius-s, 4px);
|
|
9655
|
+
font-size: var(--text-s);
|
|
9656
|
+
white-space: nowrap;
|
|
9657
|
+
pointer-events: none;
|
|
9658
|
+
inset: auto;
|
|
9659
|
+
}
|
|
9660
|
+
|
|
9661
|
+
.icicle-tooltip:popover-open {
|
|
9662
|
+
display: block;
|
|
9663
|
+
}
|
|
9664
|
+
|
|
9665
|
+
.empty-state {
|
|
9666
|
+
padding: var(--space-l);
|
|
9667
|
+
color: var(--surface-muted-text);
|
|
9668
|
+
font-size: var(--text-s);
|
|
9669
|
+
}
|
|
9670
|
+
`;
|
|
9671
|
+
//#endregion
|
|
8880
9672
|
//#region src/icicle-tree/icicletree-list.ts
|
|
8881
9673
|
let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
8882
9674
|
constructor(..._args) {
|
|
@@ -8894,6 +9686,14 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8894
9686
|
this.disableKeyboardNavigation = false;
|
|
8895
9687
|
this.registry = void 0;
|
|
8896
9688
|
this._zoomKey = void 0;
|
|
9689
|
+
this.restoreLoads = true;
|
|
9690
|
+
this.#viewState = new ViewStateController(this, {
|
|
9691
|
+
kind: `icicletree-list`,
|
|
9692
|
+
capture: () => this.getViewState()
|
|
9693
|
+
});
|
|
9694
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9695
|
+
this.#keyboardCursor = false;
|
|
9696
|
+
this.#zoomPath = [];
|
|
8897
9697
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8898
9698
|
labelSelector: `.icicle-label`,
|
|
8899
9699
|
highlightKey: `incr-search-highlight`
|
|
@@ -8922,6 +9722,9 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8922
9722
|
});
|
|
8923
9723
|
this.#pendingKeys = /* @__PURE__ */ new Set();
|
|
8924
9724
|
this.#aborters = /* @__PURE__ */ new Map();
|
|
9725
|
+
this.#onScroll = () => {
|
|
9726
|
+
if (!this.#viewState.pendingScroll) this.#viewState.notify();
|
|
9727
|
+
};
|
|
8925
9728
|
}
|
|
8926
9729
|
static {
|
|
8927
9730
|
this.shadowRootOptions = {
|
|
@@ -8937,6 +9740,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8937
9740
|
if (this._zoomKey === void 0) return void 0;
|
|
8938
9741
|
return findIcicleNode(this.#effectiveRoot, this._zoomKey);
|
|
8939
9742
|
}
|
|
9743
|
+
#viewState;
|
|
9744
|
+
/** Saved selection for keys not (yet) loaded. */
|
|
9745
|
+
#pendingSelected;
|
|
9746
|
+
/** Saved zoom path not (yet) resolvable. */
|
|
9747
|
+
#pendingZoom;
|
|
9748
|
+
#pendingFocus;
|
|
9749
|
+
/** Whether the cursor was last placed by the keyboard (only then is it `focus`). */
|
|
9750
|
+
#keyboardCursor;
|
|
9751
|
+
/** Last known key path of the zoom node, so it can go pending when the node leaves. */
|
|
9752
|
+
#zoomPath;
|
|
8940
9753
|
#modelUnsubscribe;
|
|
8941
9754
|
#pendingKeys;
|
|
8942
9755
|
#aborters;
|
|
@@ -8973,6 +9786,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8973
9786
|
});
|
|
8974
9787
|
this.registry = registry;
|
|
8975
9788
|
this.#syncSearchCommands();
|
|
9789
|
+
this.#viewState.observeScrollTarget(this);
|
|
9790
|
+
this.addEventListener(`scroll`, this.#onScroll, { passive: true });
|
|
8976
9791
|
if (typeof ResizeObserver !== `undefined`) {
|
|
8977
9792
|
this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
|
|
8978
9793
|
this.#resizeObserver.observe(this);
|
|
@@ -8985,6 +9800,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8985
9800
|
for (const aborter of this.#aborters.values()) aborter.abort();
|
|
8986
9801
|
this.#aborters.clear();
|
|
8987
9802
|
this.#pendingKeys.clear();
|
|
9803
|
+
this.removeEventListener(`scroll`, this.#onScroll);
|
|
8988
9804
|
this.#resizeObserver?.disconnect();
|
|
8989
9805
|
this.#resizeObserver = void 0;
|
|
8990
9806
|
if (this.#ownedRegistry) {
|
|
@@ -9006,28 +9822,23 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9006
9822
|
this.#unregisterSearchCommands?.();
|
|
9007
9823
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
9008
9824
|
}
|
|
9009
|
-
|
|
9010
|
-
if (changedProps.has(`root`)
|
|
9011
|
-
if (changedProps.has(`
|
|
9012
|
-
this.#modelUnsubscribe?.();
|
|
9013
|
-
this.#modelUnsubscribe = void 0;
|
|
9014
|
-
if (this.model) {
|
|
9015
|
-
this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
9016
|
-
if (event.type === `root`) {
|
|
9017
|
-
this.#resetViewState();
|
|
9018
|
-
this.requestUpdate();
|
|
9019
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
9020
|
-
});
|
|
9021
|
-
this.#resetViewState();
|
|
9022
|
-
}
|
|
9023
|
-
}
|
|
9024
|
-
if (changedProps.has(`interactionMode`)) {
|
|
9825
|
+
willUpdate(changedProps) {
|
|
9826
|
+
if (changedProps.has(`root`) || changedProps.has(`model`)) this.#anchorIndex = void 0;
|
|
9827
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
|
|
9025
9828
|
this.#anchorIndex = void 0;
|
|
9026
9829
|
this.clearSelection();
|
|
9027
9830
|
}
|
|
9831
|
+
this.#reconcileViewState();
|
|
9832
|
+
}
|
|
9833
|
+
updated(changedProps) {
|
|
9834
|
+
if (changedProps.has(`model`)) {
|
|
9835
|
+
this.#modelUnsubscribe?.();
|
|
9836
|
+
this.#modelUnsubscribe = this.model?._subscribe(() => this.requestUpdate());
|
|
9837
|
+
}
|
|
9028
9838
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
9029
|
-
if (this._zoomKey !== void 0 && !findIcicleNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
9030
9839
|
this.#ensureVisibleChildren();
|
|
9840
|
+
this.#continueRestoreLoads();
|
|
9841
|
+
this.#viewState.applyPendingScroll((scroll) => applyHostScroll(this, scroll));
|
|
9031
9842
|
this.#syncTooltip();
|
|
9032
9843
|
}
|
|
9033
9844
|
/** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
|
|
@@ -9077,12 +9888,103 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9077
9888
|
popover.hidePopover();
|
|
9078
9889
|
} catch {}
|
|
9079
9890
|
}
|
|
9080
|
-
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9891
|
+
/**
|
|
9892
|
+
* Snapshot selection, zoom path, keyboard cursor and scroll as plain JSON,
|
|
9893
|
+
* keyed by `item.key`. Includes pending state for keys not currently loaded.
|
|
9894
|
+
*/
|
|
9895
|
+
getViewState(options) {
|
|
9896
|
+
const withPending = options?.includePending ?? true;
|
|
9897
|
+
const focus = this.#keyboardCursor ? this.tickled.tickledId : void 0;
|
|
9898
|
+
return this.#viewState.shape({
|
|
9899
|
+
selected: keysToArray([...this._selectedNodes].map((n) => n.item.key), withPending ? this.#pendingSelected : []),
|
|
9900
|
+
zoom: (withPending ? this.#pendingZoom : void 0) ?? (this._zoomKey === void 0 ? [] : [...this.#zoomPath]),
|
|
9901
|
+
focus: focus ?? (withPending ? this.#pendingFocus : void 0),
|
|
9902
|
+
scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? (this.isConnected ? captureHostScroll(this) : void 0)
|
|
9903
|
+
}, options);
|
|
9904
|
+
}
|
|
9905
|
+
/**
|
|
9906
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
9907
|
+
* that aren't loaded yet are applied as they appear — including via
|
|
9908
|
+
* `loadChildren` / `model` down a saved zoom path when `restoreLoads`.
|
|
9909
|
+
*/
|
|
9910
|
+
setViewState(state) {
|
|
9911
|
+
if (!this.#viewState.accepts(state)) return;
|
|
9912
|
+
this.#viewState.restoring(() => {
|
|
9913
|
+
if (state.selected) {
|
|
9914
|
+
this.#pendingSelected = capKeys(new Set(state.selected));
|
|
9915
|
+
if (this._selectedNodes.size > 0) {
|
|
9916
|
+
const prev = new Set(this._selectedNodes);
|
|
9917
|
+
this.#clearSelectionInternal();
|
|
9918
|
+
this.#dispatchSelect(prev);
|
|
9919
|
+
}
|
|
9920
|
+
}
|
|
9921
|
+
if (state.zoom) this.#pendingZoom = [...state.zoom];
|
|
9922
|
+
if (state.focus !== void 0) this.#pendingFocus = state.focus;
|
|
9923
|
+
if (state.scroll) this.#viewState.pendingScroll = state.scroll;
|
|
9924
|
+
this.#reconcileViewState();
|
|
9925
|
+
if (this.#pendingZoom !== void 0) this.zoomOut();
|
|
9926
|
+
});
|
|
9927
|
+
this.requestUpdate();
|
|
9928
|
+
}
|
|
9929
|
+
clearPendingViewState() {
|
|
9930
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9931
|
+
this.#pendingZoom = void 0;
|
|
9932
|
+
this.#pendingFocus = void 0;
|
|
9933
|
+
this.#viewState.pendingScroll = void 0;
|
|
9934
|
+
}
|
|
9935
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
9936
|
+
#reconcileViewState() {
|
|
9937
|
+
if (this._selectedNodes.size === 0 && this.#pendingSelected.size === 0 && this._zoomKey === void 0 && this.#pendingZoom === void 0 && this.tickled.tickledId === void 0 && this.#pendingFocus === void 0) return;
|
|
9938
|
+
const root = this.#effectiveRoot;
|
|
9939
|
+
let index;
|
|
9940
|
+
const nodeByKey = (key) => (index ??= indexNodesByKey(root)).get(key);
|
|
9941
|
+
this.#viewState.restoring(() => {
|
|
9942
|
+
this.#reconcileZoom(root, nodeByKey);
|
|
9943
|
+
const sel = reconcileSelection$1(this._selectedNodes, this.#pendingSelected, nodeByKey, (node) => this.#canSelect(node), this.interactionMode === `implicit`);
|
|
9944
|
+
if (sel.changed) {
|
|
9945
|
+
const prev = this._selectedNodes;
|
|
9946
|
+
const primary = this.selectedNode?.item.key;
|
|
9947
|
+
this._selectedNodes = new Set(sel.selected);
|
|
9948
|
+
this.#pendingSelected = sel.pending;
|
|
9949
|
+
this.selectedNode = [...sel.selected].find((n) => n.item.key === primary) ?? [...sel.selected].at(-1);
|
|
9950
|
+
if (sel.restored) this.#dispatchSelect(prev);
|
|
9951
|
+
else this.#viewState.notify(`data`);
|
|
9952
|
+
}
|
|
9953
|
+
this.#reconcileFocus(nodeByKey);
|
|
9954
|
+
});
|
|
9955
|
+
}
|
|
9956
|
+
#reconcileZoom(root, nodeByKey) {
|
|
9957
|
+
if (this._zoomKey !== void 0 && !nodeByKey(this._zoomKey)) {
|
|
9958
|
+
this.#pendingZoom ??= this.#zoomPath;
|
|
9959
|
+
this._zoomKey = void 0;
|
|
9960
|
+
this.#viewState.notify(`data`);
|
|
9961
|
+
}
|
|
9962
|
+
const nodes = this.#pendingZoom && resolveZoomPath$1(root, this.#pendingZoom, nodeByKey);
|
|
9963
|
+
if (!nodes) return;
|
|
9964
|
+
this.#pendingZoom = void 0;
|
|
9965
|
+
if (nodes.length === 0) this.zoomOut();
|
|
9966
|
+
else this.zoomTo(nodes.at(-1));
|
|
9967
|
+
}
|
|
9968
|
+
#reconcileFocus(nodeByKey) {
|
|
9969
|
+
const tickled = this.tickled.tickledId;
|
|
9970
|
+
if (tickled !== void 0 && !nodeByKey(tickled)) {
|
|
9971
|
+
if (this.#keyboardCursor) this.#pendingFocus ??= tickled;
|
|
9972
|
+
this.tickled.clearTickled();
|
|
9973
|
+
}
|
|
9974
|
+
const focus = this.#pendingFocus;
|
|
9975
|
+
if (focus !== void 0 && this.#rects().some((r) => r.node.item.key === focus)) {
|
|
9976
|
+
this.#pendingFocus = void 0;
|
|
9977
|
+
this.#keyboardCursor = true;
|
|
9978
|
+
this.tickled.setTickled(focus);
|
|
9979
|
+
}
|
|
9085
9980
|
}
|
|
9981
|
+
/** Load the next unloaded node along a pending zoom path (one load at a time). */
|
|
9982
|
+
#continueRestoreLoads() {
|
|
9983
|
+
if (!this.restoreLoads || this.#pendingZoom === void 0 || !this.model && !this.loadChildren) return;
|
|
9984
|
+
const next = resolveKeyPath(this.#effectiveRoot, this.#pendingZoom, (node) => node.item.isLeaf !== true);
|
|
9985
|
+
if (next.status === `load` && !this.#pendingKeys.has(next.node.item.key)) this.#loadChildren(next.node, next.depth);
|
|
9986
|
+
}
|
|
9987
|
+
#onScroll;
|
|
9086
9988
|
#ensureVisibleChildren() {
|
|
9087
9989
|
if (!this.model && !this.loadChildren) return;
|
|
9088
9990
|
for (const rect of this.#rects()) {
|
|
@@ -9126,6 +10028,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9126
10028
|
this.selectedNode = void 0;
|
|
9127
10029
|
}
|
|
9128
10030
|
#dispatchSelect(previous) {
|
|
10031
|
+
if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
|
|
10032
|
+
this.#viewState.notify();
|
|
9129
10033
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
9130
10034
|
detail: {
|
|
9131
10035
|
selected: new Set(this._selectedNodes),
|
|
@@ -9153,6 +10057,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9153
10057
|
this.requestUpdate();
|
|
9154
10058
|
}
|
|
9155
10059
|
clearSelection() {
|
|
10060
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9156
10061
|
if (this._selectedNodes.size === 0) return;
|
|
9157
10062
|
const prev = new Set(this._selectedNodes);
|
|
9158
10063
|
this.#clearSelectionInternal();
|
|
@@ -9189,11 +10094,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9189
10094
|
* Fires `zoom` and the semantic `expand` event.
|
|
9190
10095
|
*/
|
|
9191
10096
|
zoomTo(node) {
|
|
10097
|
+
this.#zoomChanged();
|
|
9192
10098
|
if (this._zoomKey === node.item.key) return;
|
|
9193
10099
|
this._zoomKey = node.item.key;
|
|
9194
10100
|
this.tickled.clearTickled();
|
|
9195
|
-
const
|
|
9196
|
-
|
|
10101
|
+
const path = findIciclePath(this.#effectiveRoot, node.item.key);
|
|
10102
|
+
this.#zoomPath = path?.slice(1).map((n) => n.item.key) ?? [node.item.key];
|
|
10103
|
+
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
9197
10104
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9198
10105
|
detail: {
|
|
9199
10106
|
node,
|
|
@@ -9217,11 +10124,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9217
10124
|
* the semantic `collapse` event for the previous zoom root.
|
|
9218
10125
|
*/
|
|
9219
10126
|
zoomOut() {
|
|
10127
|
+
this.#zoomChanged();
|
|
9220
10128
|
if (this._zoomKey === void 0) return;
|
|
9221
10129
|
const previous = this.focusedNode;
|
|
9222
10130
|
const path = this._zoomKey === void 0 ? void 0 : findIciclePath(this.#effectiveRoot, this._zoomKey);
|
|
9223
10131
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
9224
10132
|
this._zoomKey = void 0;
|
|
10133
|
+
this.#zoomPath = [];
|
|
9225
10134
|
this.tickled.clearTickled();
|
|
9226
10135
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9227
10136
|
detail: {
|
|
@@ -9241,6 +10150,11 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9241
10150
|
}));
|
|
9242
10151
|
this.requestUpdate();
|
|
9243
10152
|
}
|
|
10153
|
+
/** An explicit zoom supersedes a saved one still waiting. */
|
|
10154
|
+
#zoomChanged() {
|
|
10155
|
+
if (!this.#viewState.isRestoring) this.#pendingZoom = void 0;
|
|
10156
|
+
this.#viewState.notify();
|
|
10157
|
+
}
|
|
9244
10158
|
#canZoom(node) {
|
|
9245
10159
|
if (node.item.isLeaf === true) return false;
|
|
9246
10160
|
if (node.children !== void 0) return node.children.length > 0;
|
|
@@ -9276,7 +10190,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9276
10190
|
navigateToNode(predicate) {
|
|
9277
10191
|
const root = this.#effectiveRoot;
|
|
9278
10192
|
if (!root?.children) return false;
|
|
9279
|
-
const keyPath =
|
|
10193
|
+
const keyPath = findIcicleKeyPath(root.children, predicate);
|
|
9280
10194
|
if (!keyPath) return false;
|
|
9281
10195
|
return this.navigateTo(keyPath);
|
|
9282
10196
|
}
|
|
@@ -9287,35 +10201,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9287
10201
|
revealKey(key) {
|
|
9288
10202
|
const root = this.#effectiveRoot;
|
|
9289
10203
|
if (!root?.children) return false;
|
|
9290
|
-
const path =
|
|
10204
|
+
const path = findIcicleKeyPath(root.children, (n) => n.item.key === key);
|
|
9291
10205
|
if (!path) return false;
|
|
9292
10206
|
if (path.length > this.maxDepth) {
|
|
9293
|
-
const ancestor = this.#
|
|
10207
|
+
const ancestor = resolveIcicleKeyPath(this.#effectiveRoot, path.slice(0, path.length - this.maxDepth));
|
|
9294
10208
|
if (ancestor) this.zoomTo(ancestor);
|
|
9295
10209
|
}
|
|
9296
10210
|
this.tickled.setTickled(key);
|
|
9297
10211
|
this.requestUpdate();
|
|
9298
10212
|
return true;
|
|
9299
10213
|
}
|
|
9300
|
-
#resolveKeyPath(keys) {
|
|
9301
|
-
let current = this.#effectiveRoot;
|
|
9302
|
-
for (const key of keys) {
|
|
9303
|
-
const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
|
|
9304
|
-
if (!found) return void 0;
|
|
9305
|
-
current = found;
|
|
9306
|
-
}
|
|
9307
|
-
return current;
|
|
9308
|
-
}
|
|
9309
|
-
#findKeyPath(nodes, predicate, prefix = []) {
|
|
9310
|
-
for (const node of nodes) {
|
|
9311
|
-
const path = [...prefix, node.item.key];
|
|
9312
|
-
if (predicate(node)) return path;
|
|
9313
|
-
if (node.children?.length) {
|
|
9314
|
-
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
9315
|
-
if (hit) return hit;
|
|
9316
|
-
}
|
|
9317
|
-
}
|
|
9318
|
-
}
|
|
9319
10214
|
/**
|
|
9320
10215
|
* All nodes currently visible as rectangles, in render (band-major) order,
|
|
9321
10216
|
* with absolute depth (root's children are depth 0).
|
|
@@ -9392,44 +10287,20 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9392
10287
|
const currentIndex = tickledKey === void 0 ? -1 : rects.findIndex((r) => r.node.item.key === tickledKey);
|
|
9393
10288
|
const current = currentIndex >= 0 ? rects[currentIndex] : void 0;
|
|
9394
10289
|
const moveTo = (rect) => {
|
|
9395
|
-
if (!rect) return;
|
|
9396
10290
|
event.preventDefault();
|
|
9397
10291
|
this.tickled.setTickled(rect.node.item.key);
|
|
10292
|
+
this.#keyboardCursor = true;
|
|
10293
|
+
this.#pendingFocus = void 0;
|
|
10294
|
+
this.#viewState.notify();
|
|
9398
10295
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
9399
10296
|
};
|
|
10297
|
+
if (event.key === `ArrowLeft` || event.key === `ArrowRight`) event.preventDefault();
|
|
10298
|
+
const target = icicleKeyTarget(rects, current, event.key);
|
|
10299
|
+
if (target) {
|
|
10300
|
+
moveTo(target);
|
|
10301
|
+
return;
|
|
10302
|
+
}
|
|
9400
10303
|
switch (event.key) {
|
|
9401
|
-
case `ArrowLeft`:
|
|
9402
|
-
case `ArrowRight`: {
|
|
9403
|
-
event.preventDefault();
|
|
9404
|
-
const dir = event.key === `ArrowRight` ? 1 : -1;
|
|
9405
|
-
if (!current) {
|
|
9406
|
-
moveTo(rects[0]);
|
|
9407
|
-
return;
|
|
9408
|
-
}
|
|
9409
|
-
const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
|
|
9410
|
-
moveTo(siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1)));
|
|
9411
|
-
return;
|
|
9412
|
-
}
|
|
9413
|
-
case `ArrowUp`:
|
|
9414
|
-
if (!current) {
|
|
9415
|
-
moveTo(rects[0]);
|
|
9416
|
-
return;
|
|
9417
|
-
}
|
|
9418
|
-
moveTo((current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0]);
|
|
9419
|
-
return;
|
|
9420
|
-
case `ArrowDown`:
|
|
9421
|
-
if (!current) {
|
|
9422
|
-
moveTo(rects[0]);
|
|
9423
|
-
return;
|
|
9424
|
-
}
|
|
9425
|
-
moveTo(rects.find((r) => r.parentKey === current.node.item.key));
|
|
9426
|
-
return;
|
|
9427
|
-
case `Home`:
|
|
9428
|
-
moveTo(rects[0]);
|
|
9429
|
-
return;
|
|
9430
|
-
case `End`:
|
|
9431
|
-
moveTo(rects.at(-1));
|
|
9432
|
-
return;
|
|
9433
10304
|
case `Enter`:
|
|
9434
10305
|
if (!current) return;
|
|
9435
10306
|
event.preventDefault();
|
|
@@ -9479,6 +10350,10 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9479
10350
|
if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
|
|
9480
10351
|
else this.zoomTo(node);
|
|
9481
10352
|
}
|
|
10353
|
+
#onRectPointerEnter(key) {
|
|
10354
|
+
this.tickled.handlePointerEnter(key);
|
|
10355
|
+
if (this.tickled.tickledId === key) this.#keyboardCursor = false;
|
|
10356
|
+
}
|
|
9482
10357
|
#renderRect(rect) {
|
|
9483
10358
|
const { node } = rect;
|
|
9484
10359
|
const selected = this._selectedNodes.has(node);
|
|
@@ -9507,7 +10382,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9507
10382
|
style=${sizeStyle}
|
|
9508
10383
|
@click=${(e) => this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e)}
|
|
9509
10384
|
@dblclick=${() => this.#handleItemDblClick(rect)}
|
|
9510
|
-
@pointerenter=${() => this
|
|
10385
|
+
@pointerenter=${() => this.#onRectPointerEnter(node.item.key)}
|
|
9511
10386
|
>
|
|
9512
10387
|
<span class="icicle-label ${hideLabel ? `hide-label` : ``} ${rotated ? `label-vertical` : ``}" part="label">${node.item.label}</span>
|
|
9513
10388
|
</div>
|
|
@@ -9552,185 +10427,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9552
10427
|
tickledStyles,
|
|
9553
10428
|
tickledItemStyles,
|
|
9554
10429
|
listSearchStyles,
|
|
9555
|
-
|
|
9556
|
-
:host {
|
|
9557
|
-
--icicletree-band-height: 44px;
|
|
9558
|
-
--icicletree-band-gap: 2px;
|
|
9559
|
-
--icicletree-rect-gap: 2px;
|
|
9560
|
-
--icicletree-rect-bg: var(--surface-4);
|
|
9561
|
-
--icicletree-rect-text: var(--surface-4-text);
|
|
9562
|
-
--icicletree-font-size: var(--text-s);
|
|
9563
|
-
|
|
9564
|
-
display: block;
|
|
9565
|
-
overflow: auto;
|
|
9566
|
-
}
|
|
9567
|
-
|
|
9568
|
-
.icicle {
|
|
9569
|
-
display: flex;
|
|
9570
|
-
gap: var(--icicletree-band-gap);
|
|
9571
|
-
outline: none;
|
|
9572
|
-
}
|
|
9573
|
-
|
|
9574
|
-
.icicle.horizontal {
|
|
9575
|
-
flex-direction: column;
|
|
9576
|
-
}
|
|
9577
|
-
|
|
9578
|
-
.icicle.vertical {
|
|
9579
|
-
flex-direction: row;
|
|
9580
|
-
min-height: 200px;
|
|
9581
|
-
}
|
|
9582
|
-
|
|
9583
|
-
.breadcrumb {
|
|
9584
|
-
display: flex;
|
|
9585
|
-
align-items: center;
|
|
9586
|
-
padding: var(--space-xs) var(--space-s);
|
|
9587
|
-
background: var(--surface-3);
|
|
9588
|
-
color: var(--surface-3-text);
|
|
9589
|
-
border: var(--border-subtle);
|
|
9590
|
-
border-radius: var(--radius-m);
|
|
9591
|
-
font-size: var(--text-s);
|
|
9592
|
-
flex-shrink: 0;
|
|
9593
|
-
}
|
|
9594
|
-
|
|
9595
|
-
.breadcrumb ixfx-crumb-navigation {
|
|
9596
|
-
flex: 1;
|
|
9597
|
-
min-width: 0;
|
|
9598
|
-
}
|
|
9599
|
-
|
|
9600
|
-
.band {
|
|
9601
|
-
position: relative;
|
|
9602
|
-
}
|
|
9603
|
-
|
|
9604
|
-
.icicle.horizontal .band {
|
|
9605
|
-
height: var(--icicletree-band-height);
|
|
9606
|
-
}
|
|
9607
|
-
|
|
9608
|
-
.icicle.vertical .band {
|
|
9609
|
-
flex: 1 1 0;
|
|
9610
|
-
min-width: 0;
|
|
9611
|
-
height: 100%;
|
|
9612
|
-
}
|
|
9613
|
-
|
|
9614
|
-
.rect {
|
|
9615
|
-
box-sizing: border-box;
|
|
9616
|
-
position: absolute;
|
|
9617
|
-
overflow: hidden;
|
|
9618
|
-
display: flex;
|
|
9619
|
-
align-items: center;
|
|
9620
|
-
padding: 0 var(--space-s);
|
|
9621
|
-
background: var(--icicletree-rect-bg);
|
|
9622
|
-
color: var(--icicletree-rect-text);
|
|
9623
|
-
font-size: var(--icicletree-font-size);
|
|
9624
|
-
cursor: pointer;
|
|
9625
|
-
transition:
|
|
9626
|
-
width var(--transition),
|
|
9627
|
-
height var(--transition),
|
|
9628
|
-
left var(--transition),
|
|
9629
|
-
top var(--transition),
|
|
9630
|
-
opacity var(--transition),
|
|
9631
|
-
background-color var(--transition);
|
|
9632
|
-
}
|
|
9633
|
-
|
|
9634
|
-
.icicle.horizontal .rect {
|
|
9635
|
-
top: 0;
|
|
9636
|
-
height: 100%;
|
|
9637
|
-
}
|
|
9638
|
-
|
|
9639
|
-
/*
|
|
9640
|
-
* A rotated label needs its full allotted width to be usable — the
|
|
9641
|
-
* default left/right padding (--space-s, ~4px a side) can consume
|
|
9642
|
-
* most or all of a rect sized down to minVerticalLabelWidth, leaving
|
|
9643
|
-
* the rotated text clipped to invisible. #labelMode's width estimate
|
|
9644
|
-
* assumes the whole rect width is usable, so drop the padding here to
|
|
9645
|
-
* match that assumption instead of also budgeting for it in the
|
|
9646
|
-
* threshold (which would make already-narrow rects need even more).
|
|
9647
|
-
*/
|
|
9648
|
-
.icicle.horizontal .rect.rotated {
|
|
9649
|
-
padding: 0;
|
|
9650
|
-
}
|
|
9651
|
-
|
|
9652
|
-
.icicle.vertical .rect {
|
|
9653
|
-
left: 0;
|
|
9654
|
-
width: 100%;
|
|
9655
|
-
padding: var(--space-s) 0;
|
|
9656
|
-
justify-content: center;
|
|
9657
|
-
writing-mode: horizontal-tb;
|
|
9658
|
-
}
|
|
9659
|
-
|
|
9660
|
-
.rect.filtered {
|
|
9661
|
-
opacity: 0.3;
|
|
9662
|
-
filter: saturate(0.4);
|
|
9663
|
-
}
|
|
9664
|
-
|
|
9665
|
-
.rect.empty {
|
|
9666
|
-
padding: 0;
|
|
9667
|
-
border: none;
|
|
9668
|
-
}
|
|
9669
|
-
|
|
9670
|
-
.rect.loading {
|
|
9671
|
-
cursor: progress;
|
|
9672
|
-
opacity: 0.6;
|
|
9673
|
-
}
|
|
9674
|
-
|
|
9675
|
-
.icicle-label {
|
|
9676
|
-
overflow: hidden;
|
|
9677
|
-
text-overflow: ellipsis;
|
|
9678
|
-
white-space: nowrap;
|
|
9679
|
-
flex: 1;
|
|
9680
|
-
min-width: 0;
|
|
9681
|
-
}
|
|
9682
|
-
|
|
9683
|
-
.icicle-label.hide-label {
|
|
9684
|
-
visibility: hidden;
|
|
9685
|
-
}
|
|
9686
|
-
|
|
9687
|
-
.icicle-label.label-vertical {
|
|
9688
|
-
writing-mode: vertical-rl;
|
|
9689
|
-
text-orientation: sideways;
|
|
9690
|
-
flex: none;
|
|
9691
|
-
width: 100%;
|
|
9692
|
-
height: 100%;
|
|
9693
|
-
padding: var(--space-2xs, 2px) 0;
|
|
9694
|
-
box-sizing: border-box;
|
|
9695
|
-
display: flex;
|
|
9696
|
-
align-items: center;
|
|
9697
|
-
justify-content: center;
|
|
9698
|
-
}
|
|
9699
|
-
|
|
9700
|
-
/*
|
|
9701
|
-
* Instant tooltip for rects whose label is rotated or hidden — anchored
|
|
9702
|
-
* via CSS anchor positioning (anchor-name set on the tickled rect in
|
|
9703
|
-
* #renderRect) rather than a JS-computed position, and shown/hidden via
|
|
9704
|
-
* the Popover API rather than the browser's own (slow, un-stylable)
|
|
9705
|
-
* title tooltip.
|
|
9706
|
-
*/
|
|
9707
|
-
.icicle-tooltip {
|
|
9708
|
-
position: fixed;
|
|
9709
|
-
position-anchor: --icicle-rect-anchor;
|
|
9710
|
-
position-area: bottom center;
|
|
9711
|
-
position-try-fallbacks: top center;
|
|
9712
|
-
margin: var(--space-xs, 4px) 0 0;
|
|
9713
|
-
padding: var(--space-2xs, 2px) var(--space-xs, 6px);
|
|
9714
|
-
background: var(--surface-5);
|
|
9715
|
-
color: var(--surface-5-text);
|
|
9716
|
-
border: var(--border-subtle);
|
|
9717
|
-
border-radius: var(--radius-s, 4px);
|
|
9718
|
-
font-size: var(--text-s);
|
|
9719
|
-
white-space: nowrap;
|
|
9720
|
-
pointer-events: none;
|
|
9721
|
-
inset: auto;
|
|
9722
|
-
}
|
|
9723
|
-
|
|
9724
|
-
.icicle-tooltip:popover-open {
|
|
9725
|
-
display: block;
|
|
9726
|
-
}
|
|
9727
|
-
|
|
9728
|
-
.empty-state {
|
|
9729
|
-
padding: var(--space-l);
|
|
9730
|
-
color: var(--surface-muted-text);
|
|
9731
|
-
font-size: var(--text-s);
|
|
9732
|
-
}
|
|
9733
|
-
`
|
|
10430
|
+
icicleTreeStyles
|
|
9734
10431
|
];
|
|
9735
10432
|
}
|
|
9736
10433
|
};
|
|
@@ -9785,6 +10482,7 @@ __decorate([property({
|
|
|
9785
10482
|
reflect: true
|
|
9786
10483
|
})], IcicleTreeListElement.prototype, "searchModes", void 0);
|
|
9787
10484
|
__decorate([state()], IcicleTreeListElement.prototype, "_zoomKey", void 0);
|
|
10485
|
+
__decorate([property({ attribute: false })], IcicleTreeListElement.prototype, "restoreLoads", void 0);
|
|
9788
10486
|
__decorate([query(`#icicle-tooltip`)], IcicleTreeListElement.prototype, "_tooltipEl", void 0);
|
|
9789
10487
|
IcicleTreeListElement = __decorate([safeCustomElement(`ixfx-icicletree-list`)], IcicleTreeListElement);
|
|
9790
10488
|
//#endregion
|
|
@@ -9810,6 +10508,136 @@ function findLeafCategory(categories, key) {
|
|
|
9810
10508
|
return flattenCategories(categories).find((c) => c.key === key);
|
|
9811
10509
|
}
|
|
9812
10510
|
//#endregion
|
|
10511
|
+
//#region src/multicol-list/multicol-view-state.ts
|
|
10512
|
+
/**
|
|
10513
|
+
* Pure helpers for `ixfx-multicol-list` view state. The element runs these
|
|
10514
|
+
* when a state is restored, when `categories` change (so saved columns whose
|
|
10515
|
+
* category wasn't known yet can appear) and when a column's items arrive (so
|
|
10516
|
+
* saved selections apply once their keys exist).
|
|
10517
|
+
*/
|
|
10518
|
+
const MULTICOL_VIEW_STATE_KIND = `multicol-list`;
|
|
10519
|
+
const isSort = (v) => v === `asc` || v === `desc`;
|
|
10520
|
+
/**
|
|
10521
|
+
* Validate saved columns (they come from storage, so may be malformed).
|
|
10522
|
+
* Entries without a string `id` / `categoryKey` are dropped, as are repeat
|
|
10523
|
+
* ids; bad optional fields fall back to defaults.
|
|
10524
|
+
*/
|
|
10525
|
+
function sanitiseColumns(raw) {
|
|
10526
|
+
if (!Array.isArray(raw)) return [];
|
|
10527
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10528
|
+
const out = [];
|
|
10529
|
+
for (const entry of raw) {
|
|
10530
|
+
if (entry === null || typeof entry !== `object`) continue;
|
|
10531
|
+
const e = entry;
|
|
10532
|
+
if (typeof e.id !== `string` || typeof e.categoryKey !== `string` || seen.has(e.id)) continue;
|
|
10533
|
+
seen.add(e.id);
|
|
10534
|
+
out.push({
|
|
10535
|
+
id: e.id,
|
|
10536
|
+
categoryKey: e.categoryKey,
|
|
10537
|
+
sort: isSort(e.sort) ? e.sort : `asc`,
|
|
10538
|
+
grouped: e.grouped === true,
|
|
10539
|
+
...typeof e.menu === `boolean` ? { menu: e.menu } : {},
|
|
10540
|
+
...typeof e.width === `number` && Number.isFinite(e.width) ? { width: e.width } : {}
|
|
10541
|
+
});
|
|
10542
|
+
}
|
|
10543
|
+
return out;
|
|
10544
|
+
}
|
|
10545
|
+
/** Split saved columns into those whose category resolves now and those to keep pending. */
|
|
10546
|
+
function partitionColumns(columns, hasCategory) {
|
|
10547
|
+
const ready = [];
|
|
10548
|
+
const pending = [];
|
|
10549
|
+
columns.forEach((state, index) => {
|
|
10550
|
+
if (hasCategory(state.categoryKey)) ready.push(state);
|
|
10551
|
+
else pending.push({
|
|
10552
|
+
state,
|
|
10553
|
+
index
|
|
10554
|
+
});
|
|
10555
|
+
});
|
|
10556
|
+
return {
|
|
10557
|
+
ready,
|
|
10558
|
+
pending
|
|
10559
|
+
};
|
|
10560
|
+
}
|
|
10561
|
+
/** Split pending columns into those whose category now resolves and those still waiting. */
|
|
10562
|
+
function resolvePendingColumns(pending, hasCategory) {
|
|
10563
|
+
if (pending.length === 0) return {
|
|
10564
|
+
ready: [],
|
|
10565
|
+
pending
|
|
10566
|
+
};
|
|
10567
|
+
const ready = pending.filter((p) => hasCategory(p.state.categoryKey));
|
|
10568
|
+
return ready.length === 0 ? {
|
|
10569
|
+
ready,
|
|
10570
|
+
pending
|
|
10571
|
+
} : {
|
|
10572
|
+
ready,
|
|
10573
|
+
pending: pending.filter((p) => !hasCategory(p.state.categoryKey))
|
|
10574
|
+
};
|
|
10575
|
+
}
|
|
10576
|
+
/** Insert pending columns back at their saved positions (clamped), for a snapshot. */
|
|
10577
|
+
function mergeColumns(live, pending) {
|
|
10578
|
+
const out = [...live];
|
|
10579
|
+
for (const p of [...pending].sort((a, b) => a.index - b.index)) out.splice(Math.min(p.index, out.length), 0, p.state);
|
|
10580
|
+
return out;
|
|
10581
|
+
}
|
|
10582
|
+
/**
|
|
10583
|
+
* - selected keys no longer in the data → pending
|
|
10584
|
+
* - pending keys now in the data → selected (appended, in pending order)
|
|
10585
|
+
*/
|
|
10586
|
+
function reconcileSelection(selected, pending, has) {
|
|
10587
|
+
const kept = selected.filter(has);
|
|
10588
|
+
const dropped = kept.length === selected.length ? [] : selected.filter((k) => !has(k));
|
|
10589
|
+
const { present, missing } = partitionKeys(pending, has);
|
|
10590
|
+
if (dropped.length === 0 && present.length === 0) return {
|
|
10591
|
+
selected,
|
|
10592
|
+
pending,
|
|
10593
|
+
restored: false,
|
|
10594
|
+
changed: false
|
|
10595
|
+
};
|
|
10596
|
+
const nextPending = dropped.length === 0 ? missing : capKeys(/* @__PURE__ */ new Set([...missing, ...dropped]));
|
|
10597
|
+
const added = present.filter((k) => !kept.includes(k));
|
|
10598
|
+
return {
|
|
10599
|
+
selected: [...kept, ...added],
|
|
10600
|
+
pending: nextPending,
|
|
10601
|
+
restored: added.length > 0,
|
|
10602
|
+
changed: dropped.length > 0 || added.length > 0
|
|
10603
|
+
};
|
|
10604
|
+
}
|
|
10605
|
+
/** JSON array of string keys → capped pending set; non-strings are ignored. */
|
|
10606
|
+
function keysFromJson(raw) {
|
|
10607
|
+
if (!Array.isArray(raw)) return EMPTY_KEYS;
|
|
10608
|
+
return capKeys(new Set(raw.filter((k) => typeof k === `string`)));
|
|
10609
|
+
}
|
|
10610
|
+
/** Whether `raw` looks like a {@link ScrollState}. */
|
|
10611
|
+
function isScrollState(raw) {
|
|
10612
|
+
return raw !== null && typeof raw === `object` && typeof raw.top === `number`;
|
|
10613
|
+
}
|
|
10614
|
+
/** `record` entries as a map, keeping only values `accept` allows. */
|
|
10615
|
+
function mapFromRecord(record, accept) {
|
|
10616
|
+
const out = /* @__PURE__ */ new Map();
|
|
10617
|
+
if (record === null || typeof record !== `object`) return out;
|
|
10618
|
+
for (const [key, value] of Object.entries(record)) {
|
|
10619
|
+
const v = accept(value);
|
|
10620
|
+
if (v !== void 0) out.set(key, v);
|
|
10621
|
+
}
|
|
10622
|
+
return out;
|
|
10623
|
+
}
|
|
10624
|
+
const keyOf = (el) => el.dataset.key;
|
|
10625
|
+
/** Rendered, keyed item rows inside a column's scroll host, in visual order. */
|
|
10626
|
+
function itemRows(host) {
|
|
10627
|
+
return [...host.querySelectorAll(`li[data-key]`)];
|
|
10628
|
+
}
|
|
10629
|
+
function captureColumnScroll(host) {
|
|
10630
|
+
return captureElementScroll(host, itemRows(host), keyOf);
|
|
10631
|
+
}
|
|
10632
|
+
/** Apply `scroll` to a column's host; `true` once the position was fully reached. */
|
|
10633
|
+
function restoreColumnScroll(host, scroll) {
|
|
10634
|
+
const anchor = scroll.anchor;
|
|
10635
|
+
if (!anchor) return scrollToOffset(host, scroll.top);
|
|
10636
|
+
const row = itemRows(host).find((el) => keyOf(el) === anchor.key);
|
|
10637
|
+
if (!row) return false;
|
|
10638
|
+
return restoreElementScroll(host, row, anchor.offset);
|
|
10639
|
+
}
|
|
10640
|
+
//#endregion
|
|
9813
10641
|
//#region src/multicol-list/types.ts
|
|
9814
10642
|
const ALL_KEY = `__all__`;
|
|
9815
10643
|
//#endregion
|
|
@@ -9861,6 +10689,7 @@ var MulticolColumnController = class {
|
|
|
9861
10689
|
#dataProvider;
|
|
9862
10690
|
#onSelectionChange;
|
|
9863
10691
|
#onRequestData;
|
|
10692
|
+
#onScroll;
|
|
9864
10693
|
#abortController = void 0;
|
|
9865
10694
|
#host;
|
|
9866
10695
|
#allRow;
|
|
@@ -9868,7 +10697,19 @@ var MulticolColumnController = class {
|
|
|
9868
10697
|
#selectedValues = [];
|
|
9869
10698
|
#allSelected = true;
|
|
9870
10699
|
#lastItems = [];
|
|
10700
|
+
/** Keys of `#lastItems`; empty until data for the current category arrived. */
|
|
10701
|
+
#itemKeys = EMPTY_KEYS;
|
|
10702
|
+
/** Saved selection keys not in the data (yet). */
|
|
10703
|
+
#pendingSelection = EMPTY_KEYS;
|
|
10704
|
+
/** Saved scroll waiting for its anchor row / enough content. */
|
|
10705
|
+
#pendingScroll;
|
|
10706
|
+
#disposeScrollIntent;
|
|
9871
10707
|
#syncingSelection = false;
|
|
10708
|
+
/**
|
|
10709
|
+
* Pending `#restoreAfterSync` passes. The lister re-renders new items and
|
|
10710
|
+
* may fire `select` before the sync runs; that isn't the user's doing.
|
|
10711
|
+
*/
|
|
10712
|
+
#syncsInFlight = 0;
|
|
9872
10713
|
#state;
|
|
9873
10714
|
constructor(options) {
|
|
9874
10715
|
this.id = options.id;
|
|
@@ -9878,6 +10719,7 @@ var MulticolColumnController = class {
|
|
|
9878
10719
|
this.#dataProvider = options.dataProvider;
|
|
9879
10720
|
this.#onSelectionChange = options.onSelectionChange;
|
|
9880
10721
|
this.#onRequestData = options.onRequestData;
|
|
10722
|
+
this.#onScroll = options.onScroll;
|
|
9881
10723
|
this.#state = {
|
|
9882
10724
|
id: options.id,
|
|
9883
10725
|
categoryKey: options.category.key,
|
|
@@ -9953,7 +10795,10 @@ var MulticolColumnController = class {
|
|
|
9953
10795
|
grouped: category.defaults?.grouped ?? false,
|
|
9954
10796
|
menu: category.defaults?.menu ?? true
|
|
9955
10797
|
};
|
|
9956
|
-
this
|
|
10798
|
+
this.#pendingSelection = EMPTY_KEYS;
|
|
10799
|
+
this.#pendingScroll = void 0;
|
|
10800
|
+
this.#itemKeys = EMPTY_KEYS;
|
|
10801
|
+
this.#setAllSelection(`data`);
|
|
9957
10802
|
this.#syncGrouping();
|
|
9958
10803
|
}
|
|
9959
10804
|
get listerHost() {
|
|
@@ -9978,7 +10823,7 @@ var MulticolColumnController = class {
|
|
|
9978
10823
|
allRow.appendChild(allLabel);
|
|
9979
10824
|
const applyAll = () => {
|
|
9980
10825
|
if (this.#allSelected) return;
|
|
9981
|
-
this.#setAllSelection();
|
|
10826
|
+
this.#setAllSelection(`user`);
|
|
9982
10827
|
};
|
|
9983
10828
|
allRow.addEventListener(`click`, applyAll);
|
|
9984
10829
|
allRow.addEventListener(`keydown`, (e) => {
|
|
@@ -9999,6 +10844,10 @@ var MulticolColumnController = class {
|
|
|
9999
10844
|
this.#syncGrouping();
|
|
10000
10845
|
this.#syncAllRow();
|
|
10001
10846
|
host.append(allRow, lister);
|
|
10847
|
+
host.addEventListener(`scroll`, this.#handleScroll, { passive: true });
|
|
10848
|
+
this.#disposeScrollIntent = onUserScrollIntent(host, () => {
|
|
10849
|
+
this.#pendingScroll = void 0;
|
|
10850
|
+
});
|
|
10002
10851
|
if (this.#lastItems.length > 0) {
|
|
10003
10852
|
this.#pushItems();
|
|
10004
10853
|
this.#restoreAfterSync();
|
|
@@ -10006,6 +10855,9 @@ var MulticolColumnController = class {
|
|
|
10006
10855
|
}
|
|
10007
10856
|
unmount() {
|
|
10008
10857
|
this.#abortPending();
|
|
10858
|
+
this.#host?.removeEventListener(`scroll`, this.#handleScroll);
|
|
10859
|
+
this.#disposeScrollIntent?.();
|
|
10860
|
+
this.#disposeScrollIntent = void 0;
|
|
10009
10861
|
this.#allRow?.remove();
|
|
10010
10862
|
this.#listerEl?.remove();
|
|
10011
10863
|
this.#allRow = void 0;
|
|
@@ -10028,17 +10880,17 @@ var MulticolColumnController = class {
|
|
|
10028
10880
|
}
|
|
10029
10881
|
#applyItems(items) {
|
|
10030
10882
|
this.#lastItems = items;
|
|
10031
|
-
const
|
|
10032
|
-
|
|
10033
|
-
this.#selectedValues
|
|
10034
|
-
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10883
|
+
const keys = new Set(items.map((i) => i.key));
|
|
10884
|
+
this.#itemKeys = keys;
|
|
10885
|
+
const r = reconcileSelection(this.#selectedValues, this.#pendingSelection, (k) => keys.has(k));
|
|
10886
|
+
this.#pendingSelection = r.pending;
|
|
10887
|
+
this.#selectedValues = r.selected;
|
|
10888
|
+
this.#allSelected = r.selected.length === 0;
|
|
10889
|
+
if (this.#listerEl) {
|
|
10890
|
+
this.#pushItems();
|
|
10891
|
+
this.#restoreAfterSync();
|
|
10038
10892
|
}
|
|
10039
|
-
this.#
|
|
10040
|
-
this.#restoreAfterSync();
|
|
10041
|
-
if (changed) this.#onSelectionChange(this.id, valid);
|
|
10893
|
+
if (r.changed) this.#onSelectionChange(this.id, r.selected, r.restored ? `restore` : `data`);
|
|
10042
10894
|
}
|
|
10043
10895
|
#pushItems() {
|
|
10044
10896
|
if (!this.#listerEl) return;
|
|
@@ -10063,7 +10915,7 @@ var MulticolColumnController = class {
|
|
|
10063
10915
|
return this.#listerEl.querySelectorAll(`:scope > .ungrouped-host > *, :scope > ixfx-panel > *`);
|
|
10064
10916
|
}
|
|
10065
10917
|
#handleSelect = () => {
|
|
10066
|
-
if (this.#syncingSelection) return;
|
|
10918
|
+
if (this.#syncingSelection || this.#syncsInFlight > 0) return;
|
|
10067
10919
|
const values = [...this.#listerEl?.selectedItems ?? []].map((i) => i.key).filter((k) => k !== ALL_KEY);
|
|
10068
10920
|
if (values.length === this.#selectedValues.length && values.every((v) => this.#selectedValues.includes(v))) {
|
|
10069
10921
|
this.#syncAllRow();
|
|
@@ -10072,9 +10924,9 @@ var MulticolColumnController = class {
|
|
|
10072
10924
|
this.#selectedValues = values;
|
|
10073
10925
|
this.#allSelected = values.length === 0;
|
|
10074
10926
|
this.#syncAllRow();
|
|
10075
|
-
this.#onSelectionChange(this.id, values);
|
|
10927
|
+
this.#onSelectionChange(this.id, values, `user`);
|
|
10076
10928
|
};
|
|
10077
|
-
#setAllSelection() {
|
|
10929
|
+
#setAllSelection(origin) {
|
|
10078
10930
|
const previous = this.#selectedValues;
|
|
10079
10931
|
this.#syncingSelection = true;
|
|
10080
10932
|
try {
|
|
@@ -10085,17 +10937,64 @@ var MulticolColumnController = class {
|
|
|
10085
10937
|
this.#syncingSelection = false;
|
|
10086
10938
|
}
|
|
10087
10939
|
this.#syncAllRow();
|
|
10088
|
-
if (previous.length > 0) this.#onSelectionChange(this.id, []);
|
|
10940
|
+
if (previous.length > 0) this.#onSelectionChange(this.id, [], origin);
|
|
10089
10941
|
}
|
|
10942
|
+
/** Back to 'All' — an explicit selection change. */
|
|
10090
10943
|
clearSelection() {
|
|
10091
|
-
this.#setAllSelection();
|
|
10944
|
+
this.#setAllSelection(`user`);
|
|
10092
10945
|
}
|
|
10093
10946
|
get selectedValues() {
|
|
10094
10947
|
return this.#selectedValues;
|
|
10095
10948
|
}
|
|
10949
|
+
get pendingSelection() {
|
|
10950
|
+
return this.#pendingSelection;
|
|
10951
|
+
}
|
|
10952
|
+
/**
|
|
10953
|
+
* Replace the selection with `keys`. Keys in the current data are selected
|
|
10954
|
+
* now; the rest stay pending until items carrying them arrive.
|
|
10955
|
+
*/
|
|
10956
|
+
restoreSelection(keys) {
|
|
10957
|
+
const previous = this.#selectedValues;
|
|
10958
|
+
const r = reconcileSelection([], capKeys(keys), (k) => this.#itemKeys.has(k));
|
|
10959
|
+
this.#pendingSelection = r.pending;
|
|
10960
|
+
this.#selectedValues = r.selected;
|
|
10961
|
+
this.#allSelected = r.selected.length === 0;
|
|
10962
|
+
this.#restoreAfterSync();
|
|
10963
|
+
this.#syncAllRow();
|
|
10964
|
+
if (!(previous.length === r.selected.length && previous.every((k) => r.selected.includes(k)))) this.#onSelectionChange(this.id, r.selected, `restore`);
|
|
10965
|
+
}
|
|
10966
|
+
clearPendingSelection() {
|
|
10967
|
+
this.#pendingSelection = EMPTY_KEYS;
|
|
10968
|
+
}
|
|
10969
|
+
get pendingScroll() {
|
|
10970
|
+
return this.#pendingScroll;
|
|
10971
|
+
}
|
|
10972
|
+
/** Current scroll of the column's list; `undefined` while unmounted. */
|
|
10973
|
+
captureScroll() {
|
|
10974
|
+
return this.#host ? captureColumnScroll(this.#host) : void 0;
|
|
10975
|
+
}
|
|
10976
|
+
/** Scroll to `scroll` now if possible, else when enough items have rendered. */
|
|
10977
|
+
restoreScroll(scroll) {
|
|
10978
|
+
this.#pendingScroll = scroll;
|
|
10979
|
+
this.#tryPendingScroll();
|
|
10980
|
+
}
|
|
10981
|
+
clearPendingScroll() {
|
|
10982
|
+
this.#pendingScroll = void 0;
|
|
10983
|
+
}
|
|
10984
|
+
#tryPendingScroll() {
|
|
10985
|
+
const scroll = this.#pendingScroll;
|
|
10986
|
+
if (!scroll || !this.#host) return;
|
|
10987
|
+
if (restoreColumnScroll(this.#host, scroll)) this.#pendingScroll = void 0;
|
|
10988
|
+
}
|
|
10989
|
+
#handleScroll = () => {
|
|
10990
|
+
if (!this.#pendingScroll) this.#onScroll?.(this.id);
|
|
10991
|
+
};
|
|
10096
10992
|
#restoreAfterSync() {
|
|
10097
10993
|
if (!this.#listerEl) return;
|
|
10098
|
-
this.#
|
|
10994
|
+
this.#syncsInFlight++;
|
|
10995
|
+
this.#listerEl.updateComplete.finally(() => {
|
|
10996
|
+
this.#syncsInFlight--;
|
|
10997
|
+
}).then(() => {
|
|
10099
10998
|
if (!this.#listerEl || !this.#host) return;
|
|
10100
10999
|
this.#syncingSelection = true;
|
|
10101
11000
|
try {
|
|
@@ -10108,6 +11007,7 @@ var MulticolColumnController = class {
|
|
|
10108
11007
|
this.#syncingSelection = false;
|
|
10109
11008
|
}
|
|
10110
11009
|
this.#syncAllRow();
|
|
11010
|
+
this.#tryPendingScroll();
|
|
10111
11011
|
});
|
|
10112
11012
|
}
|
|
10113
11013
|
#syncAllRow() {
|
|
@@ -10139,12 +11039,201 @@ function renderItem(item) {
|
|
|
10139
11039
|
}
|
|
10140
11040
|
return li;
|
|
10141
11041
|
}
|
|
11042
|
+
/** Shadow styles for `ixfx-multicol-list` (theme fallbacks / handle styles are added by the element). */
|
|
11043
|
+
const multicolListStyles = css`
|
|
11044
|
+
:host {
|
|
11045
|
+
--multicol-list-bg: var(--surface-3);
|
|
11046
|
+
--multicol-list-header-bg: var(--surface-2);
|
|
11047
|
+
--multicol-list-header-text: var(--surface-3-text);
|
|
11048
|
+
--multicol-list-border: var(--border);
|
|
11049
|
+
--multicol-list-min-column-width: ${80}px;
|
|
11050
|
+
--multicol-list-max-column-width: ${480}px;
|
|
11051
|
+
--multicol-list-header-font-size: unset;
|
|
11052
|
+
|
|
11053
|
+
display: block;
|
|
11054
|
+
overflow: hidden;
|
|
11055
|
+
/* A sideways trackpad swipe over the component must never pan it
|
|
11056
|
+
(or the page behind it, or trigger history navigation) */
|
|
11057
|
+
overscroll-behavior-x: none;
|
|
11058
|
+
background: var(--multicol-list-bg);
|
|
11059
|
+
color: var(--surface-3-text);
|
|
11060
|
+
font-family: var(--font-family);
|
|
11061
|
+
}
|
|
11062
|
+
#container {
|
|
11063
|
+
display: flex;
|
|
11064
|
+
flex-direction: row;
|
|
11065
|
+
align-items: stretch;
|
|
11066
|
+
overflow: hidden;
|
|
11067
|
+
overscroll-behavior-x: none;
|
|
11068
|
+
position: relative;
|
|
11069
|
+
height: 100%;
|
|
11070
|
+
}
|
|
11071
|
+
.column {
|
|
11072
|
+
flex-shrink: 0;
|
|
11073
|
+
display: flex;
|
|
11074
|
+
flex-direction: column;
|
|
11075
|
+
border-right: var(--multicol-list-border);
|
|
11076
|
+
min-width: var(--multicol-list-min-column-width);
|
|
11077
|
+
max-width: var(--multicol-list-max-column-width);
|
|
11078
|
+
background: var(--surface-3);
|
|
11079
|
+
}
|
|
11080
|
+
.column-header {
|
|
11081
|
+
display: flex;
|
|
11082
|
+
align-items: center;
|
|
11083
|
+
gap: var(--space-xs);
|
|
11084
|
+
height: 32px;
|
|
11085
|
+
background: var(--multicol-list-header-bg);
|
|
11086
|
+
color: var(--multicol-list-header-text);
|
|
11087
|
+
border-bottom: var(--border);
|
|
11088
|
+
position: relative;
|
|
11089
|
+
user-select: none;
|
|
11090
|
+
min-width: var(--multicol-list-min-column-width);
|
|
11091
|
+
box-sizing: border-box;
|
|
11092
|
+
}
|
|
11093
|
+
.column-header[data-dragging] {
|
|
11094
|
+
opacity: 0.4;
|
|
11095
|
+
}
|
|
11096
|
+
.reorder-handle {
|
|
11097
|
+
cursor: grab;
|
|
11098
|
+
opacity: 0;
|
|
11099
|
+
display: inline-flex;
|
|
11100
|
+
align-items: center;
|
|
11101
|
+
flex-shrink: 0;
|
|
11102
|
+
padding-left: var(--space-xs);
|
|
11103
|
+
touch-action: none;
|
|
11104
|
+
transition: opacity var(--transition, 0.15s ease-out);
|
|
11105
|
+
}
|
|
11106
|
+
.column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
|
|
11107
|
+
.reorder-handle:active {
|
|
11108
|
+
cursor: grabbing;
|
|
11109
|
+
}
|
|
11110
|
+
.reorder-handle .ixfx-handle {
|
|
11111
|
+
--ixfx-handle-length: 14px;
|
|
11112
|
+
--ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
|
|
11113
|
+
--ixfx-handle-dot-size: 3px;
|
|
11114
|
+
}
|
|
11115
|
+
.column-label {
|
|
11116
|
+
flex: 1;
|
|
11117
|
+
overflow: hidden;
|
|
11118
|
+
text-overflow: ellipsis;
|
|
11119
|
+
white-space: nowrap;
|
|
11120
|
+
padding: 0 var(--space-s);
|
|
11121
|
+
font-size: var(--multicol-list-header-font-size);
|
|
11122
|
+
font-weight: 600;
|
|
11123
|
+
background: transparent;
|
|
11124
|
+
border: none;
|
|
11125
|
+
color: inherit;
|
|
11126
|
+
cursor: pointer;
|
|
11127
|
+
text-align: left;
|
|
11128
|
+
min-width: 0;
|
|
11129
|
+
}
|
|
11130
|
+
.column-label:hover {
|
|
11131
|
+
text-decoration: underline;
|
|
11132
|
+
}
|
|
11133
|
+
.burger-btn {
|
|
11134
|
+
flex-shrink: 0;
|
|
11135
|
+
width: 28px;
|
|
11136
|
+
height: 28px;
|
|
11137
|
+
display: flex;
|
|
11138
|
+
align-items: center;
|
|
11139
|
+
justify-content: center;
|
|
11140
|
+
background: transparent;
|
|
11141
|
+
border: none;
|
|
11142
|
+
cursor: pointer;
|
|
11143
|
+
color: var(--surface-3-text);
|
|
11144
|
+
opacity: 0;
|
|
11145
|
+
transition: opacity var(--transition);
|
|
11146
|
+
border-radius: var(--radius-s);
|
|
11147
|
+
}
|
|
11148
|
+
.column:hover .burger-btn,
|
|
11149
|
+
.column-header:focus-within .burger-btn {
|
|
11150
|
+
opacity: 1;
|
|
11151
|
+
}
|
|
11152
|
+
.burger-btn:hover {
|
|
11153
|
+
background: var(--surface-4);
|
|
11154
|
+
}
|
|
11155
|
+
.resize-handle {
|
|
11156
|
+
position: absolute;
|
|
11157
|
+
right: -4px;
|
|
11158
|
+
top: 0;
|
|
11159
|
+
bottom: 0;
|
|
11160
|
+
width: 8px;
|
|
11161
|
+
cursor: col-resize;
|
|
11162
|
+
z-index: 1;
|
|
11163
|
+
display: flex;
|
|
11164
|
+
align-items: stretch;
|
|
11165
|
+
justify-content: center;
|
|
11166
|
+
touch-action: none;
|
|
11167
|
+
}
|
|
11168
|
+
.resize-handle::before {
|
|
11169
|
+
content: '';
|
|
11170
|
+
width: 2px;
|
|
11171
|
+
margin: 2px 0;
|
|
11172
|
+
border-radius: 1px;
|
|
11173
|
+
background: transparent;
|
|
11174
|
+
transition: background-color var(--transition);
|
|
11175
|
+
}
|
|
11176
|
+
.resize-handle:hover::before,
|
|
11177
|
+
.resize-handle[data-active]::before {
|
|
11178
|
+
background: var(--accent);
|
|
11179
|
+
}
|
|
11180
|
+
.lister-host {
|
|
11181
|
+
flex: 1;
|
|
11182
|
+
overflow-y: auto;
|
|
11183
|
+
overflow-x: hidden;
|
|
11184
|
+
/* Vertical scrolling only: horizontal deltas are swallowed here
|
|
11185
|
+
instead of chaining up and panning the whole component */
|
|
11186
|
+
overscroll-behavior-x: none;
|
|
11187
|
+
touch-action: pan-y;
|
|
11188
|
+
position: relative;
|
|
11189
|
+
display: flex;
|
|
11190
|
+
flex-direction: column;
|
|
11191
|
+
--list-gutter: 0;
|
|
11192
|
+
}
|
|
11193
|
+
.all-row {
|
|
11194
|
+
display: flex;
|
|
11195
|
+
align-items: center;
|
|
11196
|
+
padding: var(--space-xs) var(--space-s);
|
|
11197
|
+
cursor: pointer;
|
|
11198
|
+
user-select: none;
|
|
11199
|
+
min-height: 28px;
|
|
11200
|
+
box-sizing: border-box;
|
|
11201
|
+
outline: 1px solid transparent;
|
|
11202
|
+
background: var(--surface-3);
|
|
11203
|
+
}
|
|
11204
|
+
.all-row:hover,
|
|
11205
|
+
.all-row[data-selected] {
|
|
11206
|
+
background: var(--surface-5);
|
|
11207
|
+
}
|
|
11208
|
+
.all-row[data-selected] {
|
|
11209
|
+
outline-color: var(--accent);
|
|
11210
|
+
font-weight: 600;
|
|
11211
|
+
}
|
|
11212
|
+
ixfx-grouped-item-lister {
|
|
11213
|
+
display: block;
|
|
11214
|
+
}
|
|
11215
|
+
.drop-caret {
|
|
11216
|
+
position: absolute;
|
|
11217
|
+
top: 0;
|
|
11218
|
+
bottom: 0;
|
|
11219
|
+
width: 2px;
|
|
11220
|
+
background: var(--accent);
|
|
11221
|
+
border-radius: 1px;
|
|
11222
|
+
pointer-events: none;
|
|
11223
|
+
z-index: 10;
|
|
11224
|
+
}
|
|
11225
|
+
.item-label {
|
|
11226
|
+
flex: 1;
|
|
11227
|
+
}
|
|
11228
|
+
.count {
|
|
11229
|
+
opacity: 0.6;
|
|
11230
|
+
margin-left: var(--space-s);
|
|
11231
|
+
font-size: var(--text-s);
|
|
11232
|
+
}
|
|
11233
|
+
`;
|
|
10142
11234
|
//#endregion
|
|
10143
11235
|
//#region src/multicol-list/multicol-list.ts
|
|
10144
11236
|
registerIcon(`menu`, `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="1"/><circle cx="19" cy="12" r="1"/><circle cx="5" cy="12" r="1"/></svg>`);
|
|
10145
|
-
const DEFAULT_COLUMN_WIDTH = 220;
|
|
10146
|
-
const MIN_COLUMN_WIDTH = 80;
|
|
10147
|
-
const MAX_COLUMN_WIDTH = 480;
|
|
10148
11237
|
const DRAG_THRESHOLD = 5;
|
|
10149
11238
|
let MulticolListElement = class MulticolListElement extends LitElement {
|
|
10150
11239
|
constructor(..._args) {
|
|
@@ -10169,240 +11258,33 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10169
11258
|
ghost: null,
|
|
10170
11259
|
insert: -1
|
|
10171
11260
|
};
|
|
11261
|
+
this.#viewState = new ViewStateController(this, {
|
|
11262
|
+
kind: MULTICOL_VIEW_STATE_KIND,
|
|
11263
|
+
capture: () => this.getViewState()
|
|
11264
|
+
});
|
|
11265
|
+
this.#pendingColumns = [];
|
|
11266
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11267
|
+
this.#pendingScroll = /* @__PURE__ */ new Map();
|
|
10172
11268
|
}
|
|
10173
11269
|
static {
|
|
10174
11270
|
this.styles = [
|
|
10175
11271
|
themeFallbacks,
|
|
10176
11272
|
handleStyles,
|
|
10177
|
-
|
|
10178
|
-
:host {
|
|
10179
|
-
--multicol-list-bg: var(--surface-3);
|
|
10180
|
-
--multicol-list-header-bg: var(--surface-2);
|
|
10181
|
-
--multicol-list-header-text: var(--surface-3-text);
|
|
10182
|
-
--multicol-list-border: var(--border);
|
|
10183
|
-
--multicol-list-min-column-width: ${MIN_COLUMN_WIDTH}px;
|
|
10184
|
-
--multicol-list-max-column-width: ${MAX_COLUMN_WIDTH}px;
|
|
10185
|
-
--multicol-list-header-font-size: unset;
|
|
10186
|
-
|
|
10187
|
-
display: block;
|
|
10188
|
-
overflow: hidden;
|
|
10189
|
-
/* A sideways trackpad swipe over the component must never pan it
|
|
10190
|
-
(or the page behind it, or trigger history navigation) */
|
|
10191
|
-
overscroll-behavior-x: none;
|
|
10192
|
-
background: var(--multicol-list-bg);
|
|
10193
|
-
color: var(--surface-3-text);
|
|
10194
|
-
font-family: var(--font-family);
|
|
10195
|
-
}
|
|
10196
|
-
#container {
|
|
10197
|
-
display: flex;
|
|
10198
|
-
flex-direction: row;
|
|
10199
|
-
align-items: stretch;
|
|
10200
|
-
overflow: hidden;
|
|
10201
|
-
overscroll-behavior-x: none;
|
|
10202
|
-
position: relative;
|
|
10203
|
-
height: 100%;
|
|
10204
|
-
}
|
|
10205
|
-
.column {
|
|
10206
|
-
flex-shrink: 0;
|
|
10207
|
-
display: flex;
|
|
10208
|
-
flex-direction: column;
|
|
10209
|
-
border-right: var(--multicol-list-border);
|
|
10210
|
-
min-width: var(--multicol-list-min-column-width);
|
|
10211
|
-
max-width: var(--multicol-list-max-column-width);
|
|
10212
|
-
background: var(--surface-3);
|
|
10213
|
-
}
|
|
10214
|
-
.column-header {
|
|
10215
|
-
display: flex;
|
|
10216
|
-
align-items: center;
|
|
10217
|
-
gap: var(--space-xs);
|
|
10218
|
-
height: 32px;
|
|
10219
|
-
background: var(--multicol-list-header-bg);
|
|
10220
|
-
color: var(--multicol-list-header-text);
|
|
10221
|
-
border-bottom: var(--border);
|
|
10222
|
-
position: relative;
|
|
10223
|
-
user-select: none;
|
|
10224
|
-
min-width: var(--multicol-list-min-column-width);
|
|
10225
|
-
box-sizing: border-box;
|
|
10226
|
-
}
|
|
10227
|
-
.column-header[data-dragging] {
|
|
10228
|
-
opacity: 0.4;
|
|
10229
|
-
}
|
|
10230
|
-
.reorder-handle {
|
|
10231
|
-
cursor: grab;
|
|
10232
|
-
opacity: 0;
|
|
10233
|
-
display: inline-flex;
|
|
10234
|
-
align-items: center;
|
|
10235
|
-
flex-shrink: 0;
|
|
10236
|
-
padding-left: var(--space-xs);
|
|
10237
|
-
touch-action: none;
|
|
10238
|
-
transition: opacity var(--transition, 0.15s ease-out);
|
|
10239
|
-
}
|
|
10240
|
-
.column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
|
|
10241
|
-
.reorder-handle:active {
|
|
10242
|
-
cursor: grabbing;
|
|
10243
|
-
}
|
|
10244
|
-
.reorder-handle .ixfx-handle {
|
|
10245
|
-
--ixfx-handle-length: 14px;
|
|
10246
|
-
--ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
|
|
10247
|
-
--ixfx-handle-dot-size: 3px;
|
|
10248
|
-
}
|
|
10249
|
-
.column-label {
|
|
10250
|
-
flex: 1;
|
|
10251
|
-
overflow: hidden;
|
|
10252
|
-
text-overflow: ellipsis;
|
|
10253
|
-
white-space: nowrap;
|
|
10254
|
-
padding: 0 var(--space-s);
|
|
10255
|
-
font-size: var(--multicol-list-header-font-size);
|
|
10256
|
-
font-weight: 600;
|
|
10257
|
-
background: transparent;
|
|
10258
|
-
border: none;
|
|
10259
|
-
color: inherit;
|
|
10260
|
-
cursor: pointer;
|
|
10261
|
-
text-align: left;
|
|
10262
|
-
min-width: 0;
|
|
10263
|
-
}
|
|
10264
|
-
.column-label:hover {
|
|
10265
|
-
text-decoration: underline;
|
|
10266
|
-
}
|
|
10267
|
-
.burger-btn {
|
|
10268
|
-
flex-shrink: 0;
|
|
10269
|
-
width: 28px;
|
|
10270
|
-
height: 28px;
|
|
10271
|
-
display: flex;
|
|
10272
|
-
align-items: center;
|
|
10273
|
-
justify-content: center;
|
|
10274
|
-
background: transparent;
|
|
10275
|
-
border: none;
|
|
10276
|
-
cursor: pointer;
|
|
10277
|
-
color: var(--surface-3-text);
|
|
10278
|
-
opacity: 0;
|
|
10279
|
-
transition: opacity var(--transition);
|
|
10280
|
-
border-radius: var(--radius-s);
|
|
10281
|
-
}
|
|
10282
|
-
.column:hover .burger-btn,
|
|
10283
|
-
.column-header:focus-within .burger-btn {
|
|
10284
|
-
opacity: 1;
|
|
10285
|
-
}
|
|
10286
|
-
.burger-btn:hover {
|
|
10287
|
-
background: var(--surface-4);
|
|
10288
|
-
}
|
|
10289
|
-
.resize-handle {
|
|
10290
|
-
position: absolute;
|
|
10291
|
-
right: -4px;
|
|
10292
|
-
top: 0;
|
|
10293
|
-
bottom: 0;
|
|
10294
|
-
width: 8px;
|
|
10295
|
-
cursor: col-resize;
|
|
10296
|
-
z-index: 1;
|
|
10297
|
-
display: flex;
|
|
10298
|
-
align-items: stretch;
|
|
10299
|
-
justify-content: center;
|
|
10300
|
-
touch-action: none;
|
|
10301
|
-
}
|
|
10302
|
-
.resize-handle::before {
|
|
10303
|
-
content: '';
|
|
10304
|
-
width: 2px;
|
|
10305
|
-
margin: 2px 0;
|
|
10306
|
-
border-radius: 1px;
|
|
10307
|
-
background: transparent;
|
|
10308
|
-
transition: background-color var(--transition);
|
|
10309
|
-
}
|
|
10310
|
-
.resize-handle:hover::before,
|
|
10311
|
-
.resize-handle[data-active]::before {
|
|
10312
|
-
background: var(--accent);
|
|
10313
|
-
}
|
|
10314
|
-
.lister-host {
|
|
10315
|
-
flex: 1;
|
|
10316
|
-
overflow-y: auto;
|
|
10317
|
-
overflow-x: hidden;
|
|
10318
|
-
/* Vertical scrolling only: horizontal deltas are swallowed here
|
|
10319
|
-
instead of chaining up and panning the whole component */
|
|
10320
|
-
overscroll-behavior-x: none;
|
|
10321
|
-
touch-action: pan-y;
|
|
10322
|
-
position: relative;
|
|
10323
|
-
display: flex;
|
|
10324
|
-
flex-direction: column;
|
|
10325
|
-
--list-gutter: 0;
|
|
10326
|
-
}
|
|
10327
|
-
.all-row {
|
|
10328
|
-
display: flex;
|
|
10329
|
-
align-items: center;
|
|
10330
|
-
padding: var(--space-xs) var(--space-s);
|
|
10331
|
-
cursor: pointer;
|
|
10332
|
-
user-select: none;
|
|
10333
|
-
min-height: 28px;
|
|
10334
|
-
box-sizing: border-box;
|
|
10335
|
-
outline: 1px solid transparent;
|
|
10336
|
-
background: var(--surface-3);
|
|
10337
|
-
}
|
|
10338
|
-
.all-row:hover,
|
|
10339
|
-
.all-row[data-selected] {
|
|
10340
|
-
background: var(--surface-5);
|
|
10341
|
-
}
|
|
10342
|
-
.all-row[data-selected] {
|
|
10343
|
-
outline-color: var(--accent);
|
|
10344
|
-
font-weight: 600;
|
|
10345
|
-
}
|
|
10346
|
-
ixfx-grouped-item-lister {
|
|
10347
|
-
display: block;
|
|
10348
|
-
}
|
|
10349
|
-
.drop-caret {
|
|
10350
|
-
position: absolute;
|
|
10351
|
-
top: 0;
|
|
10352
|
-
bottom: 0;
|
|
10353
|
-
width: 2px;
|
|
10354
|
-
background: var(--accent);
|
|
10355
|
-
border-radius: 1px;
|
|
10356
|
-
pointer-events: none;
|
|
10357
|
-
z-index: 10;
|
|
10358
|
-
}
|
|
10359
|
-
.item-label {
|
|
10360
|
-
flex: 1;
|
|
10361
|
-
}
|
|
10362
|
-
.count {
|
|
10363
|
-
opacity: 0.6;
|
|
10364
|
-
margin-left: var(--space-s);
|
|
10365
|
-
font-size: var(--text-s);
|
|
10366
|
-
}
|
|
10367
|
-
`
|
|
11273
|
+
multicolListStyles
|
|
10368
11274
|
];
|
|
10369
11275
|
}
|
|
10370
|
-
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
for (const colState of states) {
|
|
10377
|
-
const existing = existingMap.get(colState.id);
|
|
10378
|
-
if (existing) {
|
|
10379
|
-
if (existing.categoryKey !== colState.categoryKey) {
|
|
10380
|
-
const newCategory = findLeafCategory(this.categories, colState.categoryKey);
|
|
10381
|
-
if (newCategory) existing.setCategory(newCategory);
|
|
10382
|
-
}
|
|
10383
|
-
existing.updateState(colState);
|
|
10384
|
-
newColumns.push(existing);
|
|
10385
|
-
existingMap.delete(colState.id);
|
|
10386
|
-
continue;
|
|
10387
|
-
}
|
|
10388
|
-
const category = findLeafCategory(this.categories, colState.categoryKey);
|
|
10389
|
-
if (!category) continue;
|
|
10390
|
-
const controller = this.#createController(colState.id, category);
|
|
10391
|
-
controller.updateState(colState);
|
|
10392
|
-
newColumns.push(controller);
|
|
10393
|
-
}
|
|
10394
|
-
for (const col of existingMap.values()) col.unmount();
|
|
10395
|
-
this._columns = newColumns;
|
|
10396
|
-
this._updateColOffsets();
|
|
10397
|
-
this._emitColumnsChange(`add`);
|
|
10398
|
-
this._requeryAllColumns();
|
|
10399
|
-
this.requestUpdate();
|
|
10400
|
-
}
|
|
11276
|
+
#viewState;
|
|
11277
|
+
/** Saved columns whose category isn't in `categories` (yet). */
|
|
11278
|
+
#pendingColumns;
|
|
11279
|
+
/** Saved selection / scroll for column ids that don't exist (yet). */
|
|
11280
|
+
#pendingSelected;
|
|
11281
|
+
#pendingScroll;
|
|
10401
11282
|
addColumn(categoryKey, position) {
|
|
10402
11283
|
if (this.maxColumns > 0 && this._columns.length >= this.maxColumns) return;
|
|
10403
11284
|
const category = findLeafCategory(this.categories, categoryKey);
|
|
10404
11285
|
if (!category) return;
|
|
10405
|
-
|
|
11286
|
+
let id = `col-${++this._idCounter}`;
|
|
11287
|
+
while (this._columns.some((c) => c.id === id) || this.#pendingColumns.some((p) => p.state.id === id)) id = `col-${++this._idCounter}`;
|
|
10406
11288
|
const controller = this.#createController(id, category);
|
|
10407
11289
|
if (position !== void 0 && position >= 0 && position < this._columns.length) this._columns.splice(position, 0, controller);
|
|
10408
11290
|
else this._columns.push(controller);
|
|
@@ -10430,9 +11312,15 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10430
11312
|
}));
|
|
10431
11313
|
}
|
|
10432
11314
|
clearSelection() {
|
|
11315
|
+
this.#dropPendingSelection();
|
|
10433
11316
|
for (const col of this._columns) col.clearSelection();
|
|
10434
11317
|
this.requestUpdate();
|
|
10435
11318
|
}
|
|
11319
|
+
createRenderRoot() {
|
|
11320
|
+
const root = super.createRenderRoot();
|
|
11321
|
+
root.addEventListener(VIEW_STATE_CHANGE, (e) => e.stopPropagation());
|
|
11322
|
+
return root;
|
|
11323
|
+
}
|
|
10436
11324
|
render() {
|
|
10437
11325
|
return html`
|
|
10438
11326
|
<div id="container">
|
|
@@ -10441,7 +11329,136 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10441
11329
|
</div>
|
|
10442
11330
|
`;
|
|
10443
11331
|
}
|
|
11332
|
+
/**
|
|
11333
|
+
* Snapshot the column layout (in display order) and each column's
|
|
11334
|
+
* selection and scroll, keyed by column id. Includes pending state —
|
|
11335
|
+
* saved columns whose category isn't known and keys not yet in the data.
|
|
11336
|
+
*/
|
|
11337
|
+
getViewState(options) {
|
|
11338
|
+
const withPending = options?.includePending ?? true;
|
|
11339
|
+
const selectedByColumn = {};
|
|
11340
|
+
const scrollByColumn = {};
|
|
11341
|
+
if (withPending) {
|
|
11342
|
+
for (const [id, keys] of this.#pendingSelected) selectedByColumn[id] = [...keys];
|
|
11343
|
+
for (const [id, scroll] of this.#pendingScroll) scrollByColumn[id] = scroll;
|
|
11344
|
+
}
|
|
11345
|
+
for (const col of this._columns) {
|
|
11346
|
+
const keys = keysToArray(col.selectedValues, withPending ? col.pendingSelection : []);
|
|
11347
|
+
if (keys.length > 0) selectedByColumn[col.id] = keys;
|
|
11348
|
+
const scroll = (withPending ? col.pendingScroll : void 0) ?? col.captureScroll();
|
|
11349
|
+
if (scroll) scrollByColumn[col.id] = scroll;
|
|
11350
|
+
}
|
|
11351
|
+
return this.#viewState.shape({
|
|
11352
|
+
columns: mergeColumns(this._columns.map((c) => c.getState()), withPending ? this.#pendingColumns : []),
|
|
11353
|
+
selectedByColumn,
|
|
11354
|
+
scrollByColumn
|
|
11355
|
+
}, options);
|
|
11356
|
+
}
|
|
11357
|
+
/**
|
|
11358
|
+
* Apply saved view state; fields present replace the current ones.
|
|
11359
|
+
* `columns` creates, reorders, re-points and removes columns to match
|
|
11360
|
+
* (columns whose category isn't in `categories` wait until it is).
|
|
11361
|
+
* Selection keys not in a column's data are applied when they arrive.
|
|
11362
|
+
*/
|
|
11363
|
+
setViewState(state) {
|
|
11364
|
+
if (!this.#viewState.accepts(state)) return;
|
|
11365
|
+
this.#viewState.restoring(() => {
|
|
11366
|
+
if (state.columns) this.#applyColumns(sanitiseColumns(state.columns));
|
|
11367
|
+
if (state.selectedByColumn) {
|
|
11368
|
+
const wanted = mapFromRecord(state.selectedByColumn, (v) => Array.isArray(v) ? keysFromJson(v) : void 0);
|
|
11369
|
+
for (const col of this._columns) {
|
|
11370
|
+
col.restoreSelection(wanted.get(col.id) ?? EMPTY_KEYS);
|
|
11371
|
+
wanted.delete(col.id);
|
|
11372
|
+
}
|
|
11373
|
+
this.#pendingSelected = wanted;
|
|
11374
|
+
}
|
|
11375
|
+
if (state.scrollByColumn) {
|
|
11376
|
+
const wanted = mapFromRecord(state.scrollByColumn, (v) => isScrollState(v) ? v : void 0);
|
|
11377
|
+
for (const col of this._columns) {
|
|
11378
|
+
const scroll = wanted.get(col.id);
|
|
11379
|
+
if (scroll) col.restoreScroll(scroll);
|
|
11380
|
+
else col.clearPendingScroll();
|
|
11381
|
+
wanted.delete(col.id);
|
|
11382
|
+
}
|
|
11383
|
+
this.#pendingScroll = wanted;
|
|
11384
|
+
}
|
|
11385
|
+
this.#viewState.notify();
|
|
11386
|
+
});
|
|
11387
|
+
this.requestUpdate();
|
|
11388
|
+
}
|
|
11389
|
+
clearPendingViewState() {
|
|
11390
|
+
this.#pendingColumns = [];
|
|
11391
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11392
|
+
this.#pendingScroll = /* @__PURE__ */ new Map();
|
|
11393
|
+
for (const col of this._columns) {
|
|
11394
|
+
col.clearPendingSelection();
|
|
11395
|
+
col.clearPendingScroll();
|
|
11396
|
+
}
|
|
11397
|
+
}
|
|
11398
|
+
/** Reuse live columns by id (re-pointing their category), create the rest, drop the others. */
|
|
11399
|
+
#applyColumns(saved) {
|
|
11400
|
+
const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
|
|
11401
|
+
const { ready, pending } = partitionColumns(saved, hasCategory);
|
|
11402
|
+
const existing = new Map(this._columns.map((c) => [c.id, c]));
|
|
11403
|
+
const next = ready.map((colState) => this.#columnFromState(colState, existing));
|
|
11404
|
+
for (const col of existing.values()) col.unmount();
|
|
11405
|
+
if (saved.length > 0) this._autoSeeded = true;
|
|
11406
|
+
this.#pendingColumns = pending;
|
|
11407
|
+
this._columns = next;
|
|
11408
|
+
this._updateColOffsets();
|
|
11409
|
+
this._emitColumnsChange(`add`);
|
|
11410
|
+
this._requeryAllColumns();
|
|
11411
|
+
}
|
|
11412
|
+
/** Live column for `colState`: the existing one with that id (taken from `existing`), or a new one. */
|
|
11413
|
+
#columnFromState(colState, existing) {
|
|
11414
|
+
const category = findLeafCategory(this.categories, colState.categoryKey);
|
|
11415
|
+
const live = existing.get(colState.id);
|
|
11416
|
+
existing.delete(colState.id);
|
|
11417
|
+
if (live) {
|
|
11418
|
+
if (live.categoryKey !== colState.categoryKey) live.setCategory(category);
|
|
11419
|
+
live.updateState(colState);
|
|
11420
|
+
return live;
|
|
11421
|
+
}
|
|
11422
|
+
const controller = this.#createController(colState.id, category);
|
|
11423
|
+
controller.updateState(colState);
|
|
11424
|
+
const keys = this.#pendingSelected.get(colState.id);
|
|
11425
|
+
if (keys) {
|
|
11426
|
+
controller.restoreSelection(keys);
|
|
11427
|
+
this.#pendingSelected = withoutKey(this.#pendingSelected, colState.id);
|
|
11428
|
+
}
|
|
11429
|
+
const scroll = this.#pendingScroll.get(colState.id);
|
|
11430
|
+
if (scroll) {
|
|
11431
|
+
controller.restoreScroll(scroll);
|
|
11432
|
+
this.#pendingScroll = withoutKey(this.#pendingScroll, colState.id);
|
|
11433
|
+
}
|
|
11434
|
+
return controller;
|
|
11435
|
+
}
|
|
11436
|
+
/** Insert pending columns whose category has now appeared, at their saved positions. */
|
|
11437
|
+
#resolvePendingColumns() {
|
|
11438
|
+
const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
|
|
11439
|
+
const { ready, pending } = resolvePendingColumns(this.#pendingColumns, hasCategory);
|
|
11440
|
+
if (ready.length === 0) return;
|
|
11441
|
+
this.#viewState.restoring(() => {
|
|
11442
|
+
this.#pendingColumns = pending;
|
|
11443
|
+
const columns = [...this._columns];
|
|
11444
|
+
const none = /* @__PURE__ */ new Map();
|
|
11445
|
+
for (const p of [...ready].sort((a, b) => a.index - b.index)) {
|
|
11446
|
+
if (columns.some((c) => c.id === p.state.id)) continue;
|
|
11447
|
+
columns.splice(Math.min(p.index, columns.length), 0, this.#columnFromState(p.state, none));
|
|
11448
|
+
}
|
|
11449
|
+
this._columns = columns;
|
|
11450
|
+
this._updateColOffsets();
|
|
11451
|
+
this._emitColumnsChange(`add`);
|
|
11452
|
+
this._requeryAllColumns();
|
|
11453
|
+
});
|
|
11454
|
+
}
|
|
11455
|
+
/** An explicit selection change supersedes any saved selection still waiting. */
|
|
11456
|
+
#dropPendingSelection() {
|
|
11457
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11458
|
+
for (const col of this._columns) col.clearPendingSelection();
|
|
11459
|
+
}
|
|
10444
11460
|
updated(changedProperties) {
|
|
11461
|
+
if (changedProperties.has(`categories`)) this.#resolvePendingColumns();
|
|
10445
11462
|
const seedCategory = flattenCategories(this.categories)[0];
|
|
10446
11463
|
if (!this._autoSeeded && this._columns.length === 0 && seedCategory) {
|
|
10447
11464
|
this._autoSeeded = true;
|
|
@@ -10458,8 +11475,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10458
11475
|
this._updateColOffsets();
|
|
10459
11476
|
}
|
|
10460
11477
|
_renderColumn(col, idx) {
|
|
10461
|
-
const rawWidth = col.getState().width ??
|
|
10462
|
-
const width = Number.isFinite(rawWidth) ? rawWidth :
|
|
11478
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11479
|
+
const width = Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10463
11480
|
return html`
|
|
10464
11481
|
<div
|
|
10465
11482
|
class="column"
|
|
@@ -10521,7 +11538,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10521
11538
|
selectionMode: this.selectionMode,
|
|
10522
11539
|
dataProvider: this.dataProvider,
|
|
10523
11540
|
onSelectionChange: this._onColumnSelectionChange.bind(this),
|
|
10524
|
-
onRequestData: this._onColumnRequestData.bind(this)
|
|
11541
|
+
onRequestData: this._onColumnRequestData.bind(this),
|
|
11542
|
+
onScroll: () => this.#viewState.notify()
|
|
10525
11543
|
});
|
|
10526
11544
|
}
|
|
10527
11545
|
_onHeaderPointerDown(e, col, idx) {
|
|
@@ -10600,11 +11618,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10600
11618
|
e.preventDefault();
|
|
10601
11619
|
e.stopPropagation();
|
|
10602
11620
|
const startX = e.clientX;
|
|
10603
|
-
const rawStartW = col.getState().width ??
|
|
10604
|
-
const startW = Number.isFinite(rawStartW) ? rawStartW :
|
|
11621
|
+
const rawStartW = col.getState().width ?? 220;
|
|
11622
|
+
const startW = Number.isFinite(rawStartW) ? rawStartW : 220;
|
|
10605
11623
|
this._resizingColumnId = col.id;
|
|
10606
11624
|
const applyWidth = (clientX) => {
|
|
10607
|
-
const w = Math.max(
|
|
11625
|
+
const w = Math.max(80, Math.min(480, startW + clientX - startX));
|
|
10608
11626
|
col.setWidth(w);
|
|
10609
11627
|
return w;
|
|
10610
11628
|
};
|
|
@@ -10733,8 +11751,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10733
11751
|
this._requeryAllColumns();
|
|
10734
11752
|
this.requestUpdate();
|
|
10735
11753
|
}
|
|
10736
|
-
_onColumnSelectionChange(columnId, _values) {
|
|
11754
|
+
_onColumnSelectionChange(columnId, _values, origin) {
|
|
10737
11755
|
if (!this._columns.find((c) => c.id === columnId)) return;
|
|
11756
|
+
if (origin === `user`) this.#dropPendingSelection();
|
|
11757
|
+
this.#viewState.notify(origin);
|
|
10738
11758
|
for (const otherCol of this._columns) if (otherCol.id !== columnId) otherCol.requestData(this._buildQuery(otherCol));
|
|
10739
11759
|
const detail = {
|
|
10740
11760
|
columnId,
|
|
@@ -10769,22 +11789,23 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10769
11789
|
col.setCategory(leaves[0]);
|
|
10770
11790
|
changed = true;
|
|
10771
11791
|
}
|
|
10772
|
-
if (changed) this._emitColumnsChange(`category`);
|
|
11792
|
+
if (changed) this._emitColumnsChange(`category`, `data`);
|
|
10773
11793
|
}
|
|
10774
11794
|
_updateColOffsets() {
|
|
10775
11795
|
const offsets = [0];
|
|
10776
11796
|
let sum = 0;
|
|
10777
11797
|
for (const col of this._columns) {
|
|
10778
|
-
const rawWidth = col.getState().width ??
|
|
10779
|
-
sum += Number.isFinite(rawWidth) ? rawWidth :
|
|
11798
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11799
|
+
sum += Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10780
11800
|
offsets.push(sum);
|
|
10781
11801
|
}
|
|
10782
11802
|
if (!(offsets.length === this._colOffsets.length && offsets.every((v, i) => v === this._colOffsets[i] || Number.isNaN(v) && Number.isNaN(this._colOffsets[i])))) this._colOffsets = offsets;
|
|
10783
11803
|
}
|
|
10784
|
-
_emitColumnsChange(reason) {
|
|
11804
|
+
_emitColumnsChange(reason, viewStateReason = `user`) {
|
|
11805
|
+
this.#viewState.notify(viewStateReason);
|
|
10785
11806
|
this.dispatchEvent(new CustomEvent(`multicol-columns-change`, {
|
|
10786
11807
|
detail: {
|
|
10787
|
-
columns: this.
|
|
11808
|
+
columns: this._columns.map((c) => c.getState()),
|
|
10788
11809
|
reason
|
|
10789
11810
|
},
|
|
10790
11811
|
bubbles: true,
|
|
@@ -10794,8 +11815,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10794
11815
|
async autoSizeColumns(options) {
|
|
10795
11816
|
return new Promise((resolve) => {
|
|
10796
11817
|
setTimeout(() => {
|
|
10797
|
-
const minWidth = options?.minWidth ??
|
|
10798
|
-
const maxWidth = options?.maxWidth ??
|
|
11818
|
+
const minWidth = options?.minWidth ?? 80;
|
|
11819
|
+
const maxWidth = options?.maxWidth ?? 480;
|
|
10799
11820
|
const measure = (text) => {
|
|
10800
11821
|
const span = document.createElement(`span`);
|
|
10801
11822
|
span.style.position = `absolute`;
|
|
@@ -10873,6 +11894,11 @@ __decorate([state()], MulticolListElement.prototype, "_draggingColumnId", void 0
|
|
|
10873
11894
|
__decorate([state()], MulticolListElement.prototype, "_dropCaretX", void 0);
|
|
10874
11895
|
__decorate([query(`#container`)], MulticolListElement.prototype, "_containerEl", void 0);
|
|
10875
11896
|
MulticolListElement = __decorate([safeCustomElement(`ixfx-multicol-list`)], MulticolListElement);
|
|
11897
|
+
function withoutKey(map, key) {
|
|
11898
|
+
const next = new Map(map);
|
|
11899
|
+
next.delete(key);
|
|
11900
|
+
return next;
|
|
11901
|
+
}
|
|
10876
11902
|
//#endregion
|
|
10877
11903
|
//#region src/notifier/pill-renderer.ts
|
|
10878
11904
|
const ROW_SPACING = 8;
|
|
@@ -14535,6 +15561,242 @@ function computeTreemapLayout(root, options = {}) {
|
|
|
14535
15561
|
return rects;
|
|
14536
15562
|
}
|
|
14537
15563
|
//#endregion
|
|
15564
|
+
//#region src/treemap/treemap-view-state.ts
|
|
15565
|
+
function indexTreemapKeys(root) {
|
|
15566
|
+
const out = /* @__PURE__ */ new Map();
|
|
15567
|
+
if (!root) return out;
|
|
15568
|
+
const walk = (node, parentKey, depth) => {
|
|
15569
|
+
if (!out.has(node.item.key)) out.set(node.item.key, {
|
|
15570
|
+
node,
|
|
15571
|
+
parentKey,
|
|
15572
|
+
depth
|
|
15573
|
+
});
|
|
15574
|
+
for (const child of node.children ?? []) walk(child, node.item.key, depth + 1);
|
|
15575
|
+
};
|
|
15576
|
+
walk(root, void 0, -1);
|
|
15577
|
+
return out;
|
|
15578
|
+
}
|
|
15579
|
+
/** Key path from the root's children down to `key` (`[]` for the root itself). */
|
|
15580
|
+
function treemapKeyPath(index, key) {
|
|
15581
|
+
const path = [];
|
|
15582
|
+
for (let entry = index.get(key); entry !== void 0; entry = entry.parentKey === void 0 ? void 0 : index.get(entry.parentKey)) {
|
|
15583
|
+
if (entry.parentKey === void 0) return path;
|
|
15584
|
+
path.unshift(entry.node.item.key);
|
|
15585
|
+
}
|
|
15586
|
+
}
|
|
15587
|
+
/**
|
|
15588
|
+
* Where a saved zoom path stands against the loaded tree. The target is
|
|
15589
|
+
* matched by key anywhere (it may have moved); failing that, the deepest
|
|
15590
|
+
* loaded ancestor on the path is reported for loading when its children
|
|
15591
|
+
* haven't been fetched.
|
|
15592
|
+
*/
|
|
15593
|
+
function resolveZoomPath(index, path) {
|
|
15594
|
+
const target = path.at(-1);
|
|
15595
|
+
if (target === void 0) return {
|
|
15596
|
+
kind: `found`,
|
|
15597
|
+
key: void 0
|
|
15598
|
+
};
|
|
15599
|
+
if (index.has(target)) return {
|
|
15600
|
+
kind: `found`,
|
|
15601
|
+
key: target
|
|
15602
|
+
};
|
|
15603
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
15604
|
+
const entry = index.get(path[i]);
|
|
15605
|
+
if (!entry) continue;
|
|
15606
|
+
const { node, depth } = entry;
|
|
15607
|
+
return node.children === void 0 && node.item.isLeaf !== true ? {
|
|
15608
|
+
kind: `load`,
|
|
15609
|
+
node,
|
|
15610
|
+
depth
|
|
15611
|
+
} : { kind: `missing` };
|
|
15612
|
+
}
|
|
15613
|
+
return { kind: `missing` };
|
|
15614
|
+
}
|
|
15615
|
+
/**
|
|
15616
|
+
* Zoom: a live zoom root that left the tree goes pending (view falls back to
|
|
15617
|
+
* the full tree); a pending path whose target is now loaded is applied.
|
|
15618
|
+
* Selection: see `reconcileSelection` in `common/view-state/tree.ts`.
|
|
15619
|
+
*/
|
|
15620
|
+
function reconcileTreemapViewState(input) {
|
|
15621
|
+
const { index } = input;
|
|
15622
|
+
let zoom;
|
|
15623
|
+
let pendingZoom = input.pendingZoom;
|
|
15624
|
+
if (input.zoomKey !== void 0 && !index.has(input.zoomKey)) {
|
|
15625
|
+
pendingZoom ??= input.zoomPath;
|
|
15626
|
+
zoom = {
|
|
15627
|
+
key: void 0,
|
|
15628
|
+
path: [],
|
|
15629
|
+
restored: false
|
|
15630
|
+
};
|
|
15631
|
+
} else if (input.zoomKey !== void 0) {
|
|
15632
|
+
const path = treemapKeyPath(index, input.zoomKey) ?? input.zoomPath;
|
|
15633
|
+
if (!samePath(path, input.zoomPath)) zoom = {
|
|
15634
|
+
key: input.zoomKey,
|
|
15635
|
+
path,
|
|
15636
|
+
restored: false
|
|
15637
|
+
};
|
|
15638
|
+
}
|
|
15639
|
+
if (pendingZoom) {
|
|
15640
|
+
const resolved = resolveZoomPath(index, pendingZoom);
|
|
15641
|
+
if (resolved.kind === `found`) {
|
|
15642
|
+
const key = resolved.key;
|
|
15643
|
+
zoom = {
|
|
15644
|
+
key,
|
|
15645
|
+
path: key === void 0 ? [] : treemapKeyPath(index, key) ?? pendingZoom,
|
|
15646
|
+
restored: true
|
|
15647
|
+
};
|
|
15648
|
+
pendingZoom = void 0;
|
|
15649
|
+
}
|
|
15650
|
+
}
|
|
15651
|
+
const selection = reconcileSelection$1(input.selected, input.pendingSelected, (key) => index.get(key)?.node, input.canSelect, input.singleSelect);
|
|
15652
|
+
return {
|
|
15653
|
+
zoom,
|
|
15654
|
+
pendingZoom,
|
|
15655
|
+
selection
|
|
15656
|
+
};
|
|
15657
|
+
}
|
|
15658
|
+
function samePath(a, b) {
|
|
15659
|
+
return a.length === b.length && a.every((k, i) => k === b[i]);
|
|
15660
|
+
}
|
|
15661
|
+
//#endregion
|
|
15662
|
+
//#region src/treemap/treemap-view-state-binding.ts
|
|
15663
|
+
function createTreemapViewState(host) {
|
|
15664
|
+
const { controller } = host;
|
|
15665
|
+
/** Key path of the zoom root as last seen — what goes pending if it leaves the tree. */
|
|
15666
|
+
let zoomPath = [];
|
|
15667
|
+
let pendingZoom;
|
|
15668
|
+
let pendingSelected = EMPTY_KEYS;
|
|
15669
|
+
let pendingFocus;
|
|
15670
|
+
/** The cursor was last moved by keyboard / search (only then is it saved as `focus`). */
|
|
15671
|
+
let keyboardCursor = false;
|
|
15672
|
+
let structureVersion = 0;
|
|
15673
|
+
let reconciledVersion = -1;
|
|
15674
|
+
let cache;
|
|
15675
|
+
const index = () => {
|
|
15676
|
+
if (cache?.version !== structureVersion) cache = {
|
|
15677
|
+
version: structureVersion,
|
|
15678
|
+
index: indexTreemapKeys(host.root())
|
|
15679
|
+
};
|
|
15680
|
+
return cache.index;
|
|
15681
|
+
};
|
|
15682
|
+
const cursorKey = () => keyboardCursor ? host.tickled() : void 0;
|
|
15683
|
+
const applyZoom = (key, path, restored) => {
|
|
15684
|
+
zoomPath = path;
|
|
15685
|
+
if (host.zoomKey() === key) return;
|
|
15686
|
+
host.setZoom(key, restored);
|
|
15687
|
+
controller.notify();
|
|
15688
|
+
};
|
|
15689
|
+
const reconcileCursor$1 = (idx) => {
|
|
15690
|
+
const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => idx.has(key), host.isVisible);
|
|
15691
|
+
pendingFocus = cursor.pending;
|
|
15692
|
+
const tickled = host.tickled();
|
|
15693
|
+
if (cursor.clear || tickled !== void 0 && !idx.has(tickled)) host.setTickled(void 0);
|
|
15694
|
+
if (cursor.apply !== void 0) {
|
|
15695
|
+
host.setTickled(cursor.apply);
|
|
15696
|
+
keyboardCursor = true;
|
|
15697
|
+
controller.notify();
|
|
15698
|
+
}
|
|
15699
|
+
};
|
|
15700
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
15701
|
+
const reconcile = () => {
|
|
15702
|
+
reconciledVersion = structureVersion;
|
|
15703
|
+
if (!host.root()) return;
|
|
15704
|
+
const idx = index();
|
|
15705
|
+
controller.restoring(() => {
|
|
15706
|
+
const r = reconcileTreemapViewState({
|
|
15707
|
+
index: idx,
|
|
15708
|
+
zoomKey: host.zoomKey(),
|
|
15709
|
+
zoomPath,
|
|
15710
|
+
pendingZoom,
|
|
15711
|
+
selected: host.selected(),
|
|
15712
|
+
pendingSelected,
|
|
15713
|
+
canSelect: host.canSelect,
|
|
15714
|
+
singleSelect: host.singleSelect()
|
|
15715
|
+
});
|
|
15716
|
+
pendingZoom = r.pendingZoom;
|
|
15717
|
+
if (r.zoom) applyZoom(r.zoom.key, r.zoom.path, r.zoom.restored);
|
|
15718
|
+
const sel = r.selection;
|
|
15719
|
+
if (sel.changed) {
|
|
15720
|
+
pendingSelected = sel.pending;
|
|
15721
|
+
const primaryKey = host.selectedNode()?.item.key;
|
|
15722
|
+
const primary = [...sel.selected].find((n) => n.item.key === primaryKey) ?? [...sel.selected].at(-1);
|
|
15723
|
+
host.setSelection(sel.selected, primary, sel.restored);
|
|
15724
|
+
}
|
|
15725
|
+
reconcileCursor$1(idx);
|
|
15726
|
+
});
|
|
15727
|
+
};
|
|
15728
|
+
return {
|
|
15729
|
+
/** The structure changed (new root / model, invalidation, lazy load). */
|
|
15730
|
+
invalidate() {
|
|
15731
|
+
structureVersion++;
|
|
15732
|
+
},
|
|
15733
|
+
/** Run in `willUpdate`. `zoomGone` — the zoom root vanished through in-place mutation. */
|
|
15734
|
+
reconcileIfNeeded(zoomGone) {
|
|
15735
|
+
if (zoomGone) structureVersion++;
|
|
15736
|
+
if (reconciledVersion !== structureVersion) reconcile();
|
|
15737
|
+
},
|
|
15738
|
+
/** An explicit zoom: supersedes a saved one still waiting. */
|
|
15739
|
+
zoomed(key) {
|
|
15740
|
+
pendingZoom = void 0;
|
|
15741
|
+
zoomPath = key === void 0 ? [] : treemapKeyPath(index(), key) ?? [key];
|
|
15742
|
+
controller.notify();
|
|
15743
|
+
},
|
|
15744
|
+
/** Selection changed — explicit changes supersede a saved selection still waiting. */
|
|
15745
|
+
selectionChanged() {
|
|
15746
|
+
if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
|
|
15747
|
+
controller.notify();
|
|
15748
|
+
},
|
|
15749
|
+
clearPendingSelection() {
|
|
15750
|
+
pendingSelected = EMPTY_KEYS;
|
|
15751
|
+
},
|
|
15752
|
+
/** The cursor moved by keyboard / search navigation. */
|
|
15753
|
+
keyboardCursorMoved() {
|
|
15754
|
+
keyboardCursor = true;
|
|
15755
|
+
pendingFocus = void 0;
|
|
15756
|
+
controller.notify();
|
|
15757
|
+
},
|
|
15758
|
+
/** The cursor moved by pointer. */
|
|
15759
|
+
pointerCursorMoved() {
|
|
15760
|
+
if (!keyboardCursor) return;
|
|
15761
|
+
keyboardCursor = false;
|
|
15762
|
+
controller.notify();
|
|
15763
|
+
},
|
|
15764
|
+
get(options) {
|
|
15765
|
+
const withPending = options?.includePending ?? true;
|
|
15766
|
+
return controller.shape({
|
|
15767
|
+
selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
|
|
15768
|
+
focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
|
|
15769
|
+
zoom: [...(withPending ? pendingZoom : void 0) ?? zoomPath]
|
|
15770
|
+
}, options);
|
|
15771
|
+
},
|
|
15772
|
+
set(state) {
|
|
15773
|
+
if (!controller.accepts(state)) return;
|
|
15774
|
+
controller.restoring(() => {
|
|
15775
|
+
if (state.selected) {
|
|
15776
|
+
pendingSelected = capKeys(new Set(state.selected));
|
|
15777
|
+
if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
|
|
15778
|
+
}
|
|
15779
|
+
if (state.focus !== void 0) pendingFocus = state.focus;
|
|
15780
|
+
const zoom = pathFromJson(state.zoom);
|
|
15781
|
+
if (zoom) pendingZoom = zoom;
|
|
15782
|
+
reconcile();
|
|
15783
|
+
if (zoom && pendingZoom) applyZoom(void 0, [], true);
|
|
15784
|
+
});
|
|
15785
|
+
},
|
|
15786
|
+
clearPending() {
|
|
15787
|
+
pendingSelected = EMPTY_KEYS;
|
|
15788
|
+
pendingFocus = void 0;
|
|
15789
|
+
pendingZoom = void 0;
|
|
15790
|
+
},
|
|
15791
|
+
/** Load the next unloaded ancestor on a pending zoom path (one load at a time). */
|
|
15792
|
+
continueRestoreLoads() {
|
|
15793
|
+
if (!pendingZoom || !host.restoreLoads() || !host.canLoad()) return;
|
|
15794
|
+
const resolved = resolveZoomPath(index(), pendingZoom);
|
|
15795
|
+
if (resolved.kind === `load` && !host.isLoading(resolved.node.item.key)) host.load(resolved.node, resolved.depth);
|
|
15796
|
+
}
|
|
15797
|
+
};
|
|
15798
|
+
}
|
|
15799
|
+
//#endregion
|
|
14538
15800
|
//#region src/treemap/treemap-list.ts
|
|
14539
15801
|
let TreemapListElement = class TreemapListElement extends LitElement {
|
|
14540
15802
|
constructor(..._args) {
|
|
@@ -14556,6 +15818,33 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14556
15818
|
this.disableKeyboardNavigation = false;
|
|
14557
15819
|
this.registry = void 0;
|
|
14558
15820
|
this._zoomKey = void 0;
|
|
15821
|
+
this.restoreLoads = true;
|
|
15822
|
+
this.#viewState = createTreemapViewState({
|
|
15823
|
+
controller: new ViewStateController(this, {
|
|
15824
|
+
kind: `treemap-list`,
|
|
15825
|
+
capture: () => this.getViewState()
|
|
15826
|
+
}),
|
|
15827
|
+
root: () => this.#effectiveRoot,
|
|
15828
|
+
zoomKey: () => this._zoomKey,
|
|
15829
|
+
setZoom: (key, restored) => this.#restoreZoom(key, restored),
|
|
15830
|
+
selected: () => this._selectedNodes,
|
|
15831
|
+
selectedNode: () => this.selectedNode,
|
|
15832
|
+
canSelect: (node) => this.#canSelect(node),
|
|
15833
|
+
singleSelect: () => this.interactionMode === `implicit`,
|
|
15834
|
+
setSelection: (next, primary, fire) => {
|
|
15835
|
+
const prev = this._selectedNodes;
|
|
15836
|
+
this._selectedNodes = new Set(next);
|
|
15837
|
+
this.selectedNode = primary;
|
|
15838
|
+
if (fire) this.#dispatchSelect(prev);
|
|
15839
|
+
},
|
|
15840
|
+
tickled: () => this.tickled.tickledId,
|
|
15841
|
+
setTickled: (key) => key === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(key),
|
|
15842
|
+
isVisible: (key) => this.#rects().some((r) => r.node.item.key === key),
|
|
15843
|
+
restoreLoads: () => this.restoreLoads,
|
|
15844
|
+
canLoad: () => this.model !== void 0 || this.loadChildren !== void 0,
|
|
15845
|
+
isLoading: (key) => this.#pendingKeys.has(key),
|
|
15846
|
+
load: (node, depth) => void this.#loadChildren(node, depth)
|
|
15847
|
+
});
|
|
14559
15848
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
14560
15849
|
labelSelector: `.treemap-label`,
|
|
14561
15850
|
highlightKey: `incr-search-highlight`
|
|
@@ -14594,6 +15883,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14594
15883
|
if (this._zoomKey === void 0) return void 0;
|
|
14595
15884
|
return findTreemapNode(this.#effectiveRoot, this._zoomKey);
|
|
14596
15885
|
}
|
|
15886
|
+
#viewState;
|
|
14597
15887
|
#modelUnsubscribe;
|
|
14598
15888
|
#pendingKeys;
|
|
14599
15889
|
#aborters;
|
|
@@ -14689,40 +15979,38 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14689
15979
|
this.#unregisterSearchCommands?.();
|
|
14690
15980
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
14691
15981
|
}
|
|
15982
|
+
willUpdate(changedProps) {
|
|
15983
|
+
if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
|
|
15984
|
+
this.#viewState.reconcileIfNeeded(this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey));
|
|
15985
|
+
}
|
|
14692
15986
|
updated(changedProps) {
|
|
14693
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
14694
15987
|
if (changedProps.has(`model`)) {
|
|
14695
15988
|
this.#modelUnsubscribe?.();
|
|
14696
15989
|
this.#modelUnsubscribe = void 0;
|
|
14697
|
-
if (this.model) {
|
|
14698
|
-
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
14703
|
-
});
|
|
14704
|
-
this.#resetViewState();
|
|
14705
|
-
}
|
|
15990
|
+
if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
15991
|
+
if (event.type === `root`) this.#onDataReset();
|
|
15992
|
+
else if (event.type === `invalidate`) this.#viewState.invalidate();
|
|
15993
|
+
this.requestUpdate();
|
|
15994
|
+
});
|
|
14706
15995
|
}
|
|
14707
|
-
if (changedProps.has(`interactionMode`)) {
|
|
15996
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
|
|
14708
15997
|
this.#anchorIndex = void 0;
|
|
14709
15998
|
this.clearSelection();
|
|
14710
15999
|
}
|
|
14711
16000
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
14712
|
-
if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
14713
16001
|
this.#ensureVisibleChildren();
|
|
16002
|
+
this.#viewState.continueRestoreLoads();
|
|
14714
16003
|
if (this.#pendingFlip) {
|
|
14715
16004
|
const flip = this.#pendingFlip;
|
|
14716
16005
|
this.#pendingFlip = void 0;
|
|
14717
16006
|
this.#applyFlip(flip);
|
|
14718
16007
|
}
|
|
14719
16008
|
}
|
|
14720
|
-
|
|
16009
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
16010
|
+
#onDataReset() {
|
|
14721
16011
|
this.#pendingFlip = void 0;
|
|
14722
|
-
this.#clearSelectionInternal();
|
|
14723
|
-
this.tickled.clearTickled();
|
|
14724
|
-
this._zoomKey = void 0;
|
|
14725
16012
|
this.#anchorIndex = void 0;
|
|
16013
|
+
this.#viewState.invalidate();
|
|
14726
16014
|
}
|
|
14727
16015
|
#ensureVisibleChildren() {
|
|
14728
16016
|
if (!this.model && !this.loadChildren) return;
|
|
@@ -14753,7 +16041,10 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14753
16041
|
} finally {
|
|
14754
16042
|
this.#pendingKeys.delete(key);
|
|
14755
16043
|
this.#aborters.delete(key);
|
|
14756
|
-
if (!controller.signal.aborted)
|
|
16044
|
+
if (!controller.signal.aborted) {
|
|
16045
|
+
this.#viewState.invalidate();
|
|
16046
|
+
this.requestUpdate();
|
|
16047
|
+
}
|
|
14757
16048
|
}
|
|
14758
16049
|
}
|
|
14759
16050
|
#canSelect(node) {
|
|
@@ -14767,6 +16058,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14767
16058
|
this.selectedNode = void 0;
|
|
14768
16059
|
}
|
|
14769
16060
|
#dispatchSelect(previous) {
|
|
16061
|
+
this.#viewState.selectionChanged();
|
|
14770
16062
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
14771
16063
|
detail: {
|
|
14772
16064
|
selected: new Set(this._selectedNodes),
|
|
@@ -14794,6 +16086,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14794
16086
|
this.requestUpdate();
|
|
14795
16087
|
}
|
|
14796
16088
|
clearSelection() {
|
|
16089
|
+
this.#viewState.clearPendingSelection();
|
|
14797
16090
|
if (this._selectedNodes.size === 0) return;
|
|
14798
16091
|
const prev = new Set(this._selectedNodes);
|
|
14799
16092
|
this.#clearSelectionInternal();
|
|
@@ -14832,7 +16125,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14832
16125
|
zoomTo(node) {
|
|
14833
16126
|
if (this._zoomKey === node.item.key) return;
|
|
14834
16127
|
this.#pendingFlip = this.#planFlip(node.item.key);
|
|
14835
|
-
this
|
|
16128
|
+
this.#setZoom(node.item.key);
|
|
14836
16129
|
this.tickled.clearTickled();
|
|
14837
16130
|
const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
|
|
14838
16131
|
const absoluteDepth = depth === void 0 ? -1 : depth - 2;
|
|
@@ -14864,7 +16157,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14864
16157
|
const previous = this.focusedNode;
|
|
14865
16158
|
const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
|
|
14866
16159
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
14867
|
-
this
|
|
16160
|
+
this.#setZoom(void 0);
|
|
14868
16161
|
this.tickled.clearTickled();
|
|
14869
16162
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
14870
16163
|
detail: {
|
|
@@ -14884,6 +16177,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14884
16177
|
}));
|
|
14885
16178
|
this.requestUpdate();
|
|
14886
16179
|
}
|
|
16180
|
+
/** An explicit zoom change: supersedes any saved zoom still waiting. */
|
|
16181
|
+
#setZoom(key) {
|
|
16182
|
+
this._zoomKey = key;
|
|
16183
|
+
this.#viewState.zoomed(key);
|
|
16184
|
+
}
|
|
16185
|
+
/** Zoom set by view-state reconcile. `restored` fires `zoom` like an explicit zoom, without animating. */
|
|
16186
|
+
#restoreZoom(key, restored) {
|
|
16187
|
+
this._zoomKey = key;
|
|
16188
|
+
if (!restored) return;
|
|
16189
|
+
const path = key === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, key);
|
|
16190
|
+
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
16191
|
+
detail: {
|
|
16192
|
+
node: path?.at(-1),
|
|
16193
|
+
depth: path === void 0 ? -1 : path.length - 2
|
|
16194
|
+
},
|
|
16195
|
+
bubbles: true,
|
|
16196
|
+
composed: true
|
|
16197
|
+
}));
|
|
16198
|
+
}
|
|
14887
16199
|
/**
|
|
14888
16200
|
* Work out (before `_zoomKey` changes) whether the upcoming zoom should
|
|
14889
16201
|
* animate, and how — see `FlipPlan`. Returns `undefined` to skip the
|
|
@@ -14993,10 +16305,20 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14993
16305
|
const ancestor = this.#resolveKeyPath(path.slice(0, path.length - this.maxDepth));
|
|
14994
16306
|
if (ancestor) this.zoomTo(ancestor);
|
|
14995
16307
|
}
|
|
14996
|
-
this
|
|
16308
|
+
this.#setKeyboardCursor(key);
|
|
14997
16309
|
this.requestUpdate();
|
|
14998
16310
|
return true;
|
|
14999
16311
|
}
|
|
16312
|
+
/** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
|
|
16313
|
+
#setKeyboardCursor(key) {
|
|
16314
|
+
this.tickled.setTickled(key);
|
|
16315
|
+
this.#viewState.keyboardCursorMoved();
|
|
16316
|
+
}
|
|
16317
|
+
#onTilePointerEnter(key) {
|
|
16318
|
+
const before = this.tickled.tickledId;
|
|
16319
|
+
this.tickled.handlePointerEnter(key);
|
|
16320
|
+
if (this.tickled.tickledId !== before) this.#viewState.pointerCursorMoved();
|
|
16321
|
+
}
|
|
15000
16322
|
#resolveKeyPath(keys) {
|
|
15001
16323
|
let current = this.#effectiveRoot;
|
|
15002
16324
|
for (const key of keys) {
|
|
@@ -15026,6 +16348,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15026
16348
|
depth: r.depth
|
|
15027
16349
|
}));
|
|
15028
16350
|
}
|
|
16351
|
+
/**
|
|
16352
|
+
* Snapshot selection, zoom path and keyboard cursor as plain JSON, keyed by
|
|
16353
|
+
* `item.key`. Includes pending state for keys not currently loaded.
|
|
16354
|
+
*/
|
|
16355
|
+
getViewState(options) {
|
|
16356
|
+
return this.#viewState.get(options);
|
|
16357
|
+
}
|
|
16358
|
+
/**
|
|
16359
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
16360
|
+
* that aren't loaded yet are applied as they appear — a deep `zoom` loads
|
|
16361
|
+
* its ancestors via `loadChildren` / `model` when `restoreLoads`.
|
|
16362
|
+
*/
|
|
16363
|
+
setViewState(state) {
|
|
16364
|
+
this.#viewState.set(state);
|
|
16365
|
+
this.requestUpdate();
|
|
16366
|
+
}
|
|
16367
|
+
clearPendingViewState() {
|
|
16368
|
+
this.#viewState.clearPending();
|
|
16369
|
+
}
|
|
15029
16370
|
#matchesFilter(node, depth) {
|
|
15030
16371
|
if (!this.filterPredicate) return true;
|
|
15031
16372
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -15127,7 +16468,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15127
16468
|
const moveTo = (rect) => {
|
|
15128
16469
|
if (!rect) return;
|
|
15129
16470
|
event.preventDefault();
|
|
15130
|
-
this
|
|
16471
|
+
this.#setKeyboardCursor(rect.node.item.key);
|
|
15131
16472
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
15132
16473
|
};
|
|
15133
16474
|
switch (event.key) {
|
|
@@ -15308,7 +16649,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15308
16649
|
e.stopPropagation();
|
|
15309
16650
|
this.#handleItemDblClick(rect);
|
|
15310
16651
|
}}
|
|
15311
|
-
@pointerenter=${() => this
|
|
16652
|
+
@pointerenter=${() => this.#onTilePointerEnter(node.item.key)}
|
|
15312
16653
|
>
|
|
15313
16654
|
${showHeader ? html`<div class="tile-header" part="tile-header"><span class="treemap-label" part="label">${node.item.label}</span></div>` : html`<span class="treemap-label ${hideLabel ? `hide-label` : ``}" part="label">${node.item.label}</span>`}
|
|
15314
16655
|
${childrenContent}
|
|
@@ -15622,6 +16963,7 @@ __decorate([property({
|
|
|
15622
16963
|
reflect: true
|
|
15623
16964
|
})], TreemapListElement.prototype, "searchModes", void 0);
|
|
15624
16965
|
__decorate([state()], TreemapListElement.prototype, "_zoomKey", void 0);
|
|
16966
|
+
__decorate([property({ attribute: false })], TreemapListElement.prototype, "restoreLoads", void 0);
|
|
15625
16967
|
TreemapListElement = __decorate([safeCustomElement(`ixfx-treemap-list`)], TreemapListElement);
|
|
15626
16968
|
//#endregion
|
|
15627
16969
|
//#region src/twosplit-layout/split-layout.ts
|
|
@@ -15648,6 +16990,10 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15648
16990
|
this.#nestedSplitInitialSizeA = ``;
|
|
15649
16991
|
this.#pendingNestedUpdate = null;
|
|
15650
16992
|
this.#hysteresisThreshold = .1;
|
|
16993
|
+
this.#viewState = new ViewStateController(this, {
|
|
16994
|
+
kind: `twosplit-layout`,
|
|
16995
|
+
capture: () => this.getViewState()
|
|
16996
|
+
});
|
|
15651
16997
|
this.#onResizeHandleDown = (event) => {
|
|
15652
16998
|
event.preventDefault();
|
|
15653
16999
|
const zoneEl = this.getZone();
|
|
@@ -15664,6 +17010,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15664
17010
|
if (zoneEl) zoneEl.classList.remove(`user-resizing`);
|
|
15665
17011
|
window.removeEventListener(`pointerup`, this.#onResizeHandleUp);
|
|
15666
17012
|
window.removeEventListener(`pointermove`, this.#onResizeHandleMove);
|
|
17013
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15667
17014
|
};
|
|
15668
17015
|
this.#onResizeHandleMove = (event) => {
|
|
15669
17016
|
const bounds = this.#elementBounds;
|
|
@@ -15783,9 +17130,11 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15783
17130
|
if (nestedSplit) {
|
|
15784
17131
|
const nestedZoneEl = nestedSplit.getZone();
|
|
15785
17132
|
if (nestedZoneEl) nestedZoneEl.classList.remove(`user-resizing`);
|
|
17133
|
+
if (#viewState in nestedSplit && nestedSplit.sizeA !== this.#nestedSplitInitialSizeA) nestedSplit.#viewState.notify();
|
|
15786
17134
|
}
|
|
15787
17135
|
window.removeEventListener(`pointerup`, this.#onCornerResizeUp);
|
|
15788
17136
|
window.removeEventListener(`pointermove`, this.#onCornerResizeMove);
|
|
17137
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15789
17138
|
};
|
|
15790
17139
|
this.#onCornerResizeMove = (event) => {
|
|
15791
17140
|
const outerBounds = this.#elementBounds;
|
|
@@ -15853,6 +17202,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15853
17202
|
#collapsedAPointerAtCollapse;
|
|
15854
17203
|
#collapsedBPointerAtCollapse;
|
|
15855
17204
|
#hysteresisThreshold;
|
|
17205
|
+
#viewState;
|
|
15856
17206
|
disconnectedCallback() {
|
|
15857
17207
|
super.disconnectedCallback();
|
|
15858
17208
|
this.#observer?.disconnect();
|
|
@@ -15862,7 +17212,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15862
17212
|
}
|
|
15863
17213
|
updated(changedProperties) {
|
|
15864
17214
|
if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateCornerPosition();
|
|
15865
|
-
if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`)) this.#updateDataCollapsed();
|
|
17215
|
+
if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateDataCollapsed();
|
|
15866
17216
|
}
|
|
15867
17217
|
#updateDataCollapsed() {
|
|
15868
17218
|
if (this.collapsedA) this.setAttribute(`data-collapsed-a`, ``);
|
|
@@ -16021,6 +17371,31 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
16021
17371
|
else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) cornerZone.style.cursor = `nesw-resize`;
|
|
16022
17372
|
}
|
|
16023
17373
|
}
|
|
17374
|
+
/** Snapshot both sides' size and collapse. */
|
|
17375
|
+
getViewState(options) {
|
|
17376
|
+
return this.#viewState.shape({ layout: {
|
|
17377
|
+
a: sideState(this.sizeA, this.collapsedA),
|
|
17378
|
+
b: sideState(this.sizeB, this.collapsedB)
|
|
17379
|
+
} }, options);
|
|
17380
|
+
}
|
|
17381
|
+
/** Restore sizes / collapse. A side missing from `layout` is left as it is. */
|
|
17382
|
+
setViewState(state) {
|
|
17383
|
+
if (!this.#viewState.accepts(state) || !state.layout) return;
|
|
17384
|
+
const { a, b } = state.layout;
|
|
17385
|
+
this.#viewState.restoring(() => {
|
|
17386
|
+
if (typeof a?.size === `string`) {
|
|
17387
|
+
this.sizeA = a.size;
|
|
17388
|
+
this.collapsedA = a.collapsed === true;
|
|
17389
|
+
}
|
|
17390
|
+
if (typeof b?.size === `string`) {
|
|
17391
|
+
this.sizeB = b.size;
|
|
17392
|
+
this.collapsedB = b.collapsed === true;
|
|
17393
|
+
}
|
|
17394
|
+
this.#viewState.notify();
|
|
17395
|
+
});
|
|
17396
|
+
}
|
|
17397
|
+
/** Both slots always exist, so nothing is ever pending. */
|
|
17398
|
+
clearPendingViewState() {}
|
|
16024
17399
|
getZone() {
|
|
16025
17400
|
if (!this.shadowRoot) return;
|
|
16026
17401
|
const el = this.shadowRoot.querySelector(`#zone`);
|
|
@@ -16208,6 +17583,12 @@ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSi
|
|
|
16208
17583
|
__decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeB", void 0);
|
|
16209
17584
|
__decorate([state()], TwoSplitLayoutElement.prototype, "_hasCorner", void 0);
|
|
16210
17585
|
TwoSplitLayoutElement = __decorate([safeCustomElement(`ixfx-twosplit-layout`)], TwoSplitLayoutElement);
|
|
17586
|
+
function sideState(size, collapsed) {
|
|
17587
|
+
return collapsed ? {
|
|
17588
|
+
size,
|
|
17589
|
+
collapsed
|
|
17590
|
+
} : { size };
|
|
17591
|
+
}
|
|
16211
17592
|
//#endregion
|
|
16212
17593
|
//#region src/util/checkbox.ts
|
|
16213
17594
|
function isCheckboxLike(el) {
|
|
@@ -16266,4 +17647,4 @@ function init() {
|
|
|
16266
17647
|
console.log(`Init`);
|
|
16267
17648
|
}
|
|
16268
17649
|
//#endregion
|
|
16269
|
-
export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EMPTY_GROUP_LABEL, EditableLabel, EditableNumber, FlatListElement, FormElement, FormGroupElement, FormOptionsElement, FormPartElement, FormRowElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListGroupHeaderElement, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findEmptiedStacks, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, partitionByGroup, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveGroupKey, resolveLeafPath, resolveTracks, rotationIsometry, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, snapshotNonEmptyStacks, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toGroupKey, toSnapshot, toggleInSet, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, wrapRoot };
|
|
17650
|
+
export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, CheckedSelect, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DEFAULT_SIBLING_SPACING, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EMPTY_FLAGS, EMPTY_GROUP_LABEL, EMPTY_KEYS, EditableLabel, EditableNumber, FlatListElement, FormElement, FormGroupElement, FormOptionsElement, FormPartElement, FormRowElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListGroupHeaderElement, ListNativeDragController, ListSearchController, ListSelectionController, MAX_PENDING_KEYS, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SCROLL_HOVER_SUPPRESS_ATTR, SUNBURST_PATH_SEPARATOR, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SunburstTreeListElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VIEW_STATE_CHANGE, VIEW_STATE_VERSION, VerticalListElement, ViewStateController, XyPad, addPanelToStack, anchorFromRow, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applyIsometry, applySnapshot, applyTreeDrop, attachTimelineInteractions, bindViewState, bottomDrawerCommandId, buildHighlightTargets, buildModel, capFlags, capKeys, captionForEntry, captureElementScroll, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, composeIsometry, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeSunburstLayout, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createScrollHoverGuard, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createValueResolver, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, defaultSunburstValueAccessor, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findEmptiedStacks, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleKeyPath, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findSunburstId, findSunburstNodeById, findTreemapNode, findTreemapPath, flagsFromRecord, flagsToRecord, geodesicInterpolate, geodesicPath, geodesicSegment, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicBranchId, hyperbolicChildIds, hyperbolicDistanceForEuclideanRadius, hyperbolicIdDepth, hyperbolicRelativeFrame, iconBus, identityIsometry, init, insertBeside, invertIsometry, isDragging, isHyperbolicDescendantId, isMultiMode, isSplit, isStack, isSunburstAncestorId, isViewStateful, keyForElement, keysAsFlags, keysToArray, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, onUserScrollIntent, panelGapIndex, parseColourScaleAttr, parseLayout, partitionByGroup, partitionFlags, partitionKeys, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveGroupKey, resolveIcicleKeyPath, resolveLeafPath, resolveTracks, restoreElementScroll, rotationIsometry, scrollToOffset, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, snapshotNonEmptyStacks, startListItemOutAnimation, sunburstChildId, sunburstHasChildren, sunburstIdToPath, sunburstParentId, sunburstPathToId, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toGroupKey, toSnapshot, toggleInSet, toolbarGapIndex, translationFromOrigin, viewIsometry, walk, withoutFlag, wrapRoot };
|