@ixfx/components 0.15.5 → 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 +917 -44
- package/bundle/index.js +702 -696
- 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-D7qzmMMp.d.ts → data-grid-CIG2DVip.d.ts} +45 -3
- package/dist/{data-grid-C1sZX6n9.js → data-grid-rMkLX2qo.js} +593 -206
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-CKIKlszv.d.ts → detail-list-6iMTNP4N.d.ts} +34 -4
- 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-DXMyDiSH.d.ts → flat-list-JPgQSmLW.d.ts} +27 -4
- 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-0e7snW6t.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/incr-search.d.ts +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/{index-DEVyVIn8.d.ts → index-Bx0BXPxI.d.ts} +52 -6
- package/dist/index-CYRc6Dah.d.ts +475 -0
- package/dist/{index-KN2MW_xD.d.ts → index-Dr80Zdl6.d.ts} +1 -1
- package/dist/index.d.ts +334 -23
- package/dist/index.js +2247 -867
- 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/miller-BFRfn0ve.js +2237 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- package/dist/pending-BGidHppL.js +91 -0
- package/dist/scroll-hover-guard-Cjw6179Q.js +35 -0
- package/dist/{split-layout-JK3xWVX2.d.ts → split-layout-CophkNeU.d.ts} +41 -3
- 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 -2
- package/dist/sunburst-tree.js +1 -1
- package/dist/{sunbursttree-list-yXwS-uEI.js → sunbursttree-list-B_VxOHlo.js} +298 -79
- package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-D6j5BxVQ.d.ts} +40 -3
- 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-D8MksoTp.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 +2 -0
- package/docs-user/crumbs.md +27 -0
- package/docs-user/data-grid.md +66 -1
- 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 +12 -2
- package/docs-user/llms.txt +2 -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 +32 -2
- package/docs-user/tree.md +45 -6
- 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 +2 -0
- package/package.json +1 -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,7 +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";
|
|
67
|
-
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-
|
|
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";
|
|
68
72
|
import { SwipeElement } from "./swipe.js";
|
|
69
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";
|
|
70
74
|
import { TransitoryLabelElement } from "./transitory-label.js";
|
|
@@ -1408,6 +1412,46 @@ __decorate([state()], SplitButton.prototype, "_loadingItems", void 0);
|
|
|
1408
1412
|
__decorate([state()], SplitButton.prototype, "_menuHasItems", void 0);
|
|
1409
1413
|
SplitButton = __decorate([safeCustomElement(`ixfx-split-button`)], SplitButton);
|
|
1410
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
|
|
1411
1455
|
//#region src/crumbs/crumb-path-controller.ts
|
|
1412
1456
|
var CrumbPathController = class {
|
|
1413
1457
|
#element = null;
|
|
@@ -3515,6 +3559,46 @@ function findEmptiedStacks(before, after) {
|
|
|
3515
3559
|
return emptied;
|
|
3516
3560
|
}
|
|
3517
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
|
|
3518
3602
|
//#region src/tabs/tab-list-item.ts
|
|
3519
3603
|
const resizeHandlers = /* @__PURE__ */ new WeakMap();
|
|
3520
3604
|
const sharedResizeObserver = new ResizeObserver((entries) => {
|
|
@@ -4159,6 +4243,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4159
4243
|
this._toolbarSlots = /* @__PURE__ */ new Set();
|
|
4160
4244
|
this._toolbarObserver = null;
|
|
4161
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;
|
|
4162
4251
|
this._onDragStart = (event) => {
|
|
4163
4252
|
if (event.composedPath().find((n) => n instanceof HTMLElement && n.tagName === `IXFX-DOCK-HOST`) !== this) return;
|
|
4164
4253
|
const { panelId, pointerId, clientX, clientY } = event.detail;
|
|
@@ -4191,6 +4280,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4191
4280
|
this._onSplitWeightsChanged = () => {
|
|
4192
4281
|
this._foldLiveWeights();
|
|
4193
4282
|
this._scheduleJunctions();
|
|
4283
|
+
this.#viewState.notify();
|
|
4284
|
+
};
|
|
4285
|
+
this._onInnerViewState = (event) => {
|
|
4286
|
+
if (event.target === event.currentTarget) event.stopPropagation();
|
|
4194
4287
|
};
|
|
4195
4288
|
this._onSplitCollapsed = (event, collapsed) => {
|
|
4196
4289
|
const detail = event.detail;
|
|
@@ -4201,6 +4294,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4201
4294
|
if (node === null || !isStack(node)) return;
|
|
4202
4295
|
if (collapsed) node.collapsed = true;
|
|
4203
4296
|
else delete node.collapsed;
|
|
4297
|
+
this.#viewState.notify();
|
|
4204
4298
|
};
|
|
4205
4299
|
this._onSplitClose = (event) => {
|
|
4206
4300
|
event.preventDefault();
|
|
@@ -4223,6 +4317,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4223
4317
|
if (stack === null || !isStack(stack)) return;
|
|
4224
4318
|
if (!setActive(stack, panelId)) return;
|
|
4225
4319
|
this.requestUpdate();
|
|
4320
|
+
this.#viewState.notify();
|
|
4226
4321
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4227
4322
|
detail: { panelId },
|
|
4228
4323
|
bubbles: true,
|
|
@@ -4273,6 +4368,9 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4273
4368
|
this._teardownTabDragListeners();
|
|
4274
4369
|
};
|
|
4275
4370
|
}
|
|
4371
|
+
#viewState;
|
|
4372
|
+
/** A layout was restored via `setViewState()`; it wins over the initial `layout` attribute. */
|
|
4373
|
+
#layoutRestored;
|
|
4276
4374
|
connectedCallback() {
|
|
4277
4375
|
super.connectedCallback();
|
|
4278
4376
|
this._buildInitialModel();
|
|
@@ -4310,7 +4408,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4310
4408
|
}
|
|
4311
4409
|
willUpdate(changed) {
|
|
4312
4410
|
super.willUpdate(changed);
|
|
4313
|
-
|
|
4411
|
+
const initial = !this.hasUpdated && this.#layoutRestored;
|
|
4412
|
+
if (changed.has(`layout`) && this.layout !== null && !initial) {
|
|
4314
4413
|
const parsed = parseLayout(this.layout);
|
|
4315
4414
|
if (parsed) {
|
|
4316
4415
|
this._model = parsed.root;
|
|
@@ -4342,6 +4441,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4342
4441
|
this._scheduleJunctions();
|
|
4343
4442
|
}
|
|
4344
4443
|
_buildInitialModel() {
|
|
4444
|
+
if (this.#layoutRestored) {
|
|
4445
|
+
this._mergePoolIntoModel();
|
|
4446
|
+
return;
|
|
4447
|
+
}
|
|
4345
4448
|
if (this.layout !== null) {
|
|
4346
4449
|
const parsed = parseLayout(this.layout);
|
|
4347
4450
|
if (parsed) {
|
|
@@ -4476,21 +4579,56 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4476
4579
|
}
|
|
4477
4580
|
this._pool = current;
|
|
4478
4581
|
this.requestUpdate();
|
|
4479
|
-
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
|
+
});
|
|
4480
4617
|
}
|
|
4481
|
-
/**
|
|
4482
|
-
|
|
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() {
|
|
4483
4626
|
this._foldLiveWeights();
|
|
4484
4627
|
return serializeLayout(this._model);
|
|
4485
4628
|
}
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
this.
|
|
4489
|
-
this._model = normalize(cloneNode(layout.root));
|
|
4490
|
-
this._mergePoolIntoModel();
|
|
4491
|
-
this._liveStacks = snapshotNonEmptyStacks(this._model);
|
|
4492
|
-
this.requestUpdate();
|
|
4493
|
-
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);
|
|
4494
4632
|
}
|
|
4495
4633
|
/**
|
|
4496
4634
|
* Dock a panel. Returns false (without touching the DOM) when the
|
|
@@ -4537,11 +4675,12 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4537
4675
|
if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
|
|
4538
4676
|
if (panel.kind !== void 0) el.kind = panel.kind;
|
|
4539
4677
|
}
|
|
4678
|
+
const takesSavedPlace = target === void 0 && this._isPlaceholder(el.panelId);
|
|
4540
4679
|
if (el.parentElement !== this) this.appendChild(el);
|
|
4541
4680
|
if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
|
|
4542
4681
|
this._returnAllToolbars();
|
|
4543
|
-
if (collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
|
|
4544
|
-
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`);
|
|
4545
4684
|
else if (stackPath !== null) {
|
|
4546
4685
|
const stack = findNode(this._model, stackPath);
|
|
4547
4686
|
if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target?.index);
|
|
@@ -4582,6 +4721,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4582
4721
|
if (stack === null || !isStack(stack)) return false;
|
|
4583
4722
|
if (!setActive(stack, panelId)) return false;
|
|
4584
4723
|
this.requestUpdate();
|
|
4724
|
+
this.#viewState.notify();
|
|
4585
4725
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4586
4726
|
detail: { panelId },
|
|
4587
4727
|
bubbles: true,
|
|
@@ -4641,7 +4781,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4641
4781
|
* {@link setLeafPlacementPolicy}'s `allowEdgeSplit` (default `true`).
|
|
4642
4782
|
* Pass `[]` to seal the leaf (nothing admitted). Warns when no stack
|
|
4643
4783
|
* currently carries `name`, but the rule is still stored and applies once
|
|
4644
|
-
* one appears. Rules are host policy: kept across `
|
|
4784
|
+
* one appears. Rules are host policy: kept across `setViewState()`, never
|
|
4645
4785
|
* serialized.
|
|
4646
4786
|
*/
|
|
4647
4787
|
setLeafExclusiveContents(name, kinds) {
|
|
@@ -4688,7 +4828,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4688
4828
|
* unconstrained* leaf — the policy is not propagated to it.
|
|
4689
4829
|
*
|
|
4690
4830
|
* Warns when no stack currently carries `name`; the policy is stored and
|
|
4691
|
-
* applies once one appears. Host policy: kept across `
|
|
4831
|
+
* applies once one appears. Host policy: kept across `setViewState()`, never
|
|
4692
4832
|
* serialized.
|
|
4693
4833
|
*/
|
|
4694
4834
|
setLeafPlacementPolicy(name, policy) {
|
|
@@ -4837,7 +4977,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4837
4977
|
* already docked here or its kind is refused by `acceptsKinds`.
|
|
4838
4978
|
*/
|
|
4839
4979
|
acceptExternalPanel(snapshot, at) {
|
|
4840
|
-
|
|
4980
|
+
const placeholder = this._isPlaceholder(snapshot.panelId);
|
|
4981
|
+
if (!placeholder && collectPanelIds(this._model).includes(snapshot.panelId)) return null;
|
|
4841
4982
|
if (this.acceptsKinds !== null && !this.acceptsKinds.includes(normalizeKind(snapshot.kind))) return null;
|
|
4842
4983
|
const el = document.createElement(`ixfx-dock-panel`);
|
|
4843
4984
|
el.panelId = snapshot.panelId;
|
|
@@ -4850,7 +4991,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4850
4991
|
if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
|
|
4851
4992
|
this._returnAllToolbars();
|
|
4852
4993
|
this._pool = [...this._pool, el];
|
|
4853
|
-
this._appendPanelToFirstStack(el.panelId);
|
|
4994
|
+
if (!placeholder) this._appendPanelToFirstStack(el.panelId);
|
|
4854
4995
|
this.appendChild(el);
|
|
4855
4996
|
normalize(this._model);
|
|
4856
4997
|
this.endExternalDrop();
|
|
@@ -5346,10 +5487,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5346
5487
|
hostEmpty: now.size === 0
|
|
5347
5488
|
});
|
|
5348
5489
|
}
|
|
5349
|
-
_emitChange() {
|
|
5350
|
-
const layout = this.
|
|
5490
|
+
_emitChange(reason = `user`) {
|
|
5491
|
+
const layout = this._captureLayout();
|
|
5351
5492
|
this._announceEmptiedStacks();
|
|
5352
5493
|
this._emit(`dock-change`, { layout });
|
|
5494
|
+
this.#viewState.notify(reason);
|
|
5353
5495
|
}
|
|
5354
5496
|
render() {
|
|
5355
5497
|
return html`
|
|
@@ -5385,6 +5527,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5385
5527
|
return html`
|
|
5386
5528
|
<ixfx-split-layout
|
|
5387
5529
|
class="split"
|
|
5530
|
+
forget-absent
|
|
5388
5531
|
direction=${node.direction}
|
|
5389
5532
|
size=${size === void 0 ? nothing : live(String(size))}
|
|
5390
5533
|
style=${`--split-gutter-size: var(--dock-split-bar-size, 2px); --split-collapsed-size: var(--dock-collapsed-size, 40px)`}
|
|
@@ -5395,6 +5538,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5395
5538
|
@ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
|
|
5396
5539
|
@ixfx-split-expand=${(e) => this._onSplitCollapsed(e, false)}
|
|
5397
5540
|
@ixfx-split-close=${this._onSplitClose}
|
|
5541
|
+
@view-state-change=${this._onInnerViewState}
|
|
5398
5542
|
>
|
|
5399
5543
|
${node.children.map((child, i) => this._renderNode(child, [...path, i], node.sizes[i]))}
|
|
5400
5544
|
</ixfx-split-layout>
|
|
@@ -6768,269 +6912,6 @@ function createGestureHandlers(host) {
|
|
|
6768
6912
|
};
|
|
6769
6913
|
}
|
|
6770
6914
|
//#endregion
|
|
6771
|
-
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
6772
|
-
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
6773
|
-
const hyperbolicTreeStyles = css`
|
|
6774
|
-
:host {
|
|
6775
|
-
--hyperbolictree-node-color: var(--surface-4);
|
|
6776
|
-
--hyperbolictree-node-text: var(--surface-3-text);
|
|
6777
|
-
--hyperbolictree-edge-color: var(--surface-6);
|
|
6778
|
-
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
6779
|
-
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
6780
|
-
--hyperbolictree-font-size: 13px;
|
|
6781
|
-
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
6782
|
-
--hyperbolictree-box-text: var(--surface-4-text);
|
|
6783
|
-
--hyperbolictree-box-border: var(--surface-2);
|
|
6784
|
-
--hyperbolictree-branch-hue-start: 250;
|
|
6785
|
-
--hyperbolictree-branch-chroma: 0.13;
|
|
6786
|
-
--hyperbolictree-branch-lightness: 62%;
|
|
6787
|
-
--hyperbolictree-branch-mix: 55%;
|
|
6788
|
-
|
|
6789
|
-
display: block;
|
|
6790
|
-
position: relative;
|
|
6791
|
-
overflow: hidden;
|
|
6792
|
-
outline: none;
|
|
6793
|
-
overscroll-behavior: contain;
|
|
6794
|
-
}
|
|
6795
|
-
|
|
6796
|
-
/*
|
|
6797
|
-
* delegatesFocus means the host itself reports as focused whenever
|
|
6798
|
-
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
6799
|
-
* the host also picks up the browser's own default focus ring/fill
|
|
6800
|
-
* sized to the whole box.
|
|
6801
|
-
*/
|
|
6802
|
-
:host(:focus),
|
|
6803
|
-
:host(:focus-visible) {
|
|
6804
|
-
outline: none;
|
|
6805
|
-
}
|
|
6806
|
-
|
|
6807
|
-
.hyperbolictree {
|
|
6808
|
-
display: block;
|
|
6809
|
-
width: 100%;
|
|
6810
|
-
height: 100%;
|
|
6811
|
-
outline: none;
|
|
6812
|
-
touch-action: none;
|
|
6813
|
-
overscroll-behavior: contain;
|
|
6814
|
-
}
|
|
6815
|
-
|
|
6816
|
-
.disk-boundary {
|
|
6817
|
-
fill: none;
|
|
6818
|
-
stroke: var(--hyperbolictree-boundary-color);
|
|
6819
|
-
stroke-width: 1;
|
|
6820
|
-
}
|
|
6821
|
-
|
|
6822
|
-
.edge {
|
|
6823
|
-
fill: none;
|
|
6824
|
-
stroke: var(--hyperbolictree-edge-color);
|
|
6825
|
-
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
6826
|
-
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
6827
|
-
/* Constant on-screen width, however far the disk is zoomed. */
|
|
6828
|
-
vector-effect: non-scaling-stroke;
|
|
6829
|
-
}
|
|
6830
|
-
|
|
6831
|
-
.node {
|
|
6832
|
-
cursor: pointer;
|
|
6833
|
-
}
|
|
6834
|
-
|
|
6835
|
-
/*
|
|
6836
|
-
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
6837
|
-
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
6838
|
-
* so it stays in keeping with — and legible in — the current theme.
|
|
6839
|
-
* Only sets the base colour vars, so selected/tickled states still win.
|
|
6840
|
-
*/
|
|
6841
|
-
:host([color-by-branch]) .node,
|
|
6842
|
-
:host([color-by-branch]) .edge {
|
|
6843
|
-
--_branch-color: oklch(
|
|
6844
|
-
var(--hyperbolictree-branch-lightness)
|
|
6845
|
-
var(--hyperbolictree-branch-chroma)
|
|
6846
|
-
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
6847
|
-
);
|
|
6848
|
-
}
|
|
6849
|
-
|
|
6850
|
-
:host([color-by-branch]) .node {
|
|
6851
|
-
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6852
|
-
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
6853
|
-
}
|
|
6854
|
-
|
|
6855
|
-
:host([color-by-branch]) .edge {
|
|
6856
|
-
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
6857
|
-
}
|
|
6858
|
-
|
|
6859
|
-
.node-hit {
|
|
6860
|
-
fill: transparent;
|
|
6861
|
-
pointer-events: all;
|
|
6862
|
-
}
|
|
6863
|
-
|
|
6864
|
-
.node-dot {
|
|
6865
|
-
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
6866
|
-
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
6867
|
-
stroke: var(--surface-2);
|
|
6868
|
-
stroke-width: 1;
|
|
6869
|
-
pointer-events: none;
|
|
6870
|
-
}
|
|
6871
|
-
|
|
6872
|
-
.node-box {
|
|
6873
|
-
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
6874
|
-
stroke: var(--hyperbolictree-box-border);
|
|
6875
|
-
stroke-width: 1.5;
|
|
6876
|
-
/* Keep the border crisp however far the box has been scaled down. */
|
|
6877
|
-
vector-effect: non-scaling-stroke;
|
|
6878
|
-
}
|
|
6879
|
-
|
|
6880
|
-
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
6881
|
-
.node-box.marker {
|
|
6882
|
-
stroke-width: 1;
|
|
6883
|
-
}
|
|
6884
|
-
|
|
6885
|
-
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
6886
|
-
.hyperbolictree-label {
|
|
6887
|
-
fill: var(--hyperbolictree-node-text);
|
|
6888
|
-
cursor: pointer;
|
|
6889
|
-
user-select: none;
|
|
6890
|
-
}
|
|
6891
|
-
|
|
6892
|
-
.box-label {
|
|
6893
|
-
fill: var(--hyperbolictree-box-text);
|
|
6894
|
-
pointer-events: none;
|
|
6895
|
-
}
|
|
6896
|
-
|
|
6897
|
-
/*
|
|
6898
|
-
* State fills come paired with their theme text tokens: a box label sits
|
|
6899
|
-
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
6900
|
-
* --item-text-selected on top, not the resting box text colour.
|
|
6901
|
-
*/
|
|
6902
|
-
.node.tickled .node-dot,
|
|
6903
|
-
.node.tickled .node-box {
|
|
6904
|
-
fill: var(--item-bg-tickled);
|
|
6905
|
-
}
|
|
6906
|
-
|
|
6907
|
-
.node.tickled .box-label {
|
|
6908
|
-
fill: var(--item-text-tickled);
|
|
6909
|
-
}
|
|
6910
|
-
|
|
6911
|
-
.node.selected .node-dot,
|
|
6912
|
-
.node.selected .node-box {
|
|
6913
|
-
fill: var(--item-bg-selected);
|
|
6914
|
-
}
|
|
6915
|
-
|
|
6916
|
-
.node.selected .box-label {
|
|
6917
|
-
fill: var(--item-text-selected);
|
|
6918
|
-
}
|
|
6919
|
-
|
|
6920
|
-
.node.selected.tickled .node-dot,
|
|
6921
|
-
.node.selected.tickled .node-box {
|
|
6922
|
-
fill: var(--item-bg-selected-tickled);
|
|
6923
|
-
}
|
|
6924
|
-
|
|
6925
|
-
.node.focused .node-dot,
|
|
6926
|
-
.node.focused .node-box {
|
|
6927
|
-
stroke: var(--accent);
|
|
6928
|
-
stroke-width: 3;
|
|
6929
|
-
}
|
|
6930
|
-
|
|
6931
|
-
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
6932
|
-
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
6933
|
-
paint-order: stroke;
|
|
6934
|
-
stroke: var(--surface-2);
|
|
6935
|
-
stroke-width: 4px;
|
|
6936
|
-
stroke-linejoin: round;
|
|
6937
|
-
}
|
|
6938
|
-
|
|
6939
|
-
.node.filtered {
|
|
6940
|
-
opacity: 0.3;
|
|
6941
|
-
}
|
|
6942
|
-
|
|
6943
|
-
.node.loading .node-dot,
|
|
6944
|
-
.node.loading .node-box {
|
|
6945
|
-
opacity: 0.6;
|
|
6946
|
-
}
|
|
6947
|
-
|
|
6948
|
-
|
|
6949
|
-
/* Child preview: a clickable list of a crowded node's children. */
|
|
6950
|
-
.child-preview {
|
|
6951
|
-
position: absolute;
|
|
6952
|
-
z-index: 1;
|
|
6953
|
-
min-width: 8rem;
|
|
6954
|
-
max-width: min(18rem, 60%);
|
|
6955
|
-
max-height: 45%;
|
|
6956
|
-
overflow: auto;
|
|
6957
|
-
padding: var(--space-xs, 4px);
|
|
6958
|
-
background: var(--surface-3);
|
|
6959
|
-
color: var(--surface-3-text);
|
|
6960
|
-
border: var(--border-subtle);
|
|
6961
|
-
border-radius: var(--radius-m, 6px);
|
|
6962
|
-
box-shadow: var(--shadow-m);
|
|
6963
|
-
font-size: var(--hyperbolictree-font-size);
|
|
6964
|
-
/* Beside the node, never over it, with the dot level with the popover's middle. */
|
|
6965
|
-
transform: translate(14px, -50%);
|
|
6966
|
-
}
|
|
6967
|
-
|
|
6968
|
-
.child-preview.left {
|
|
6969
|
-
transform: translate(calc(-100% - 14px), -50%);
|
|
6970
|
-
}
|
|
6971
|
-
|
|
6972
|
-
.child-preview.below {
|
|
6973
|
-
transform: translate(-50%, 14px);
|
|
6974
|
-
}
|
|
6975
|
-
|
|
6976
|
-
.child-preview.above {
|
|
6977
|
-
transform: translate(-50%, calc(-100% - 14px));
|
|
6978
|
-
}
|
|
6979
|
-
|
|
6980
|
-
.child-preview-title {
|
|
6981
|
-
display: flex;
|
|
6982
|
-
align-items: center;
|
|
6983
|
-
gap: 4px;
|
|
6984
|
-
padding: 2px 6px 4px;
|
|
6985
|
-
font-weight: 600;
|
|
6986
|
-
}
|
|
6987
|
-
|
|
6988
|
-
.child-preview-title span {
|
|
6989
|
-
overflow: hidden;
|
|
6990
|
-
text-overflow: ellipsis;
|
|
6991
|
-
white-space: nowrap;
|
|
6992
|
-
}
|
|
6993
|
-
|
|
6994
|
-
.child-preview button.child-preview-up {
|
|
6995
|
-
display: inline-flex;
|
|
6996
|
-
flex: none;
|
|
6997
|
-
width: auto;
|
|
6998
|
-
padding: 2px;
|
|
6999
|
-
}
|
|
7000
|
-
|
|
7001
|
-
.child-preview ul {
|
|
7002
|
-
margin: 0;
|
|
7003
|
-
padding: 0;
|
|
7004
|
-
list-style: none;
|
|
7005
|
-
}
|
|
7006
|
-
|
|
7007
|
-
.child-preview button {
|
|
7008
|
-
display: block;
|
|
7009
|
-
width: 100%;
|
|
7010
|
-
padding: 2px 6px;
|
|
7011
|
-
border: none;
|
|
7012
|
-
border-radius: var(--radius-s, 4px);
|
|
7013
|
-
background: none;
|
|
7014
|
-
color: inherit;
|
|
7015
|
-
font: inherit;
|
|
7016
|
-
text-align: start;
|
|
7017
|
-
overflow: hidden;
|
|
7018
|
-
text-overflow: ellipsis;
|
|
7019
|
-
white-space: nowrap;
|
|
7020
|
-
cursor: pointer;
|
|
7021
|
-
}
|
|
7022
|
-
|
|
7023
|
-
.child-preview button:hover {
|
|
7024
|
-
background: var(--item-bg-tickled);
|
|
7025
|
-
color: var(--item-text-tickled);
|
|
7026
|
-
}
|
|
7027
|
-
|
|
7028
|
-
.child-preview-more {
|
|
7029
|
-
padding: 2px 6px;
|
|
7030
|
-
color: var(--surface-muted-text);
|
|
7031
|
-
}
|
|
7032
|
-
`;
|
|
7033
|
-
//#endregion
|
|
7034
6915
|
//#region src/hyperbolic-tree/layout.ts
|
|
7035
6916
|
const ID_SEPARATOR = `\u001F`;
|
|
7036
6917
|
/**
|
|
@@ -7445,6 +7326,564 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
7445
7326
|
};
|
|
7446
7327
|
}
|
|
7447
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
|
|
7448
7887
|
//#region src/hyperbolic-tree/keyboard.ts
|
|
7449
7888
|
const unhandled = {
|
|
7450
7889
|
action: { kind: `none` },
|
|
@@ -7543,6 +7982,11 @@ function createLazyChildLoader(source, onChange) {
|
|
|
7543
7982
|
if (!model && !loadChildren) return;
|
|
7544
7983
|
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
7545
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
|
+
},
|
|
7546
7990
|
isPending: (node) => pending.has(node),
|
|
7547
7991
|
abortAll() {
|
|
7548
7992
|
for (const aborter of aborters.values()) aborter.abort();
|
|
@@ -8063,6 +8507,35 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8063
8507
|
this.disableKeyboardNavigation = false;
|
|
8064
8508
|
this.registry = void 0;
|
|
8065
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
|
+
});
|
|
8066
8539
|
this._viewOverride = void 0;
|
|
8067
8540
|
this._focusRotation = 0;
|
|
8068
8541
|
this._zoomOffset = {
|
|
@@ -8084,7 +8557,10 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8084
8557
|
this.#lazy = createLazyChildLoader(() => ({
|
|
8085
8558
|
model: this.model,
|
|
8086
8559
|
loadChildren: this.loadChildren
|
|
8087
|
-
}), () =>
|
|
8560
|
+
}), () => {
|
|
8561
|
+
this.#viewState.invalidate();
|
|
8562
|
+
this.requestUpdate();
|
|
8563
|
+
});
|
|
8088
8564
|
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
8089
8565
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8090
8566
|
labelSelector: `.hyperbolictree-label`,
|
|
@@ -8131,6 +8607,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8131
8607
|
},
|
|
8132
8608
|
zoomState: () => this.#zoomState(),
|
|
8133
8609
|
setZoomState: (state) => {
|
|
8610
|
+
if (state.zoom !== this.zoom) this.#viewState.notify();
|
|
8134
8611
|
this.zoom = state.zoom;
|
|
8135
8612
|
this._zoomOffset = state.offset;
|
|
8136
8613
|
},
|
|
@@ -8153,6 +8630,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8153
8630
|
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
8154
8631
|
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
8155
8632
|
}
|
|
8633
|
+
#viewState;
|
|
8156
8634
|
#preview;
|
|
8157
8635
|
#lazy;
|
|
8158
8636
|
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
@@ -8260,46 +8738,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8260
8738
|
this.#unregisterSearchCommands?.();
|
|
8261
8739
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
8262
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
|
+
}
|
|
8263
8745
|
updated(changedProps) {
|
|
8264
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
8265
8746
|
if (changedProps.has(`model`)) {
|
|
8266
8747
|
this.#modelUnsubscribe?.();
|
|
8267
8748
|
this.#modelUnsubscribe = void 0;
|
|
8268
|
-
if (this.model) {
|
|
8269
|
-
|
|
8270
|
-
|
|
8271
|
-
|
|
8272
|
-
|
|
8273
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
8274
|
-
});
|
|
8275
|
-
this.#resetViewState();
|
|
8276
|
-
}
|
|
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
|
+
});
|
|
8277
8754
|
}
|
|
8278
|
-
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
8755
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) this.clearSelection();
|
|
8279
8756
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
8280
|
-
if (this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId)) {
|
|
8281
|
-
this._focusId = void 0;
|
|
8282
|
-
this._viewOverride = void 0;
|
|
8283
|
-
this._focusRotation = 0;
|
|
8284
|
-
}
|
|
8285
8757
|
this.#lazy.ensure(this.#layout().nodes);
|
|
8758
|
+
this.#viewState.continueRestoreLoads();
|
|
8286
8759
|
}
|
|
8287
|
-
|
|
8288
|
-
|
|
8289
|
-
this.tickled.clearTickled();
|
|
8290
|
-
this._focusId = void 0;
|
|
8291
|
-
this._viewOverride = void 0;
|
|
8292
|
-
this._focusRotation = 0;
|
|
8293
|
-
this._zoomOffset = {
|
|
8294
|
-
x: 0,
|
|
8295
|
-
y: 0
|
|
8296
|
-
};
|
|
8760
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
8761
|
+
#onDataReset() {
|
|
8297
8762
|
this._previewId = void 0;
|
|
8298
8763
|
this.#gestures.cancel();
|
|
8299
|
-
|
|
8300
|
-
cancelAnimationFrame(this.#animFrame);
|
|
8301
|
-
this.#animFrame = void 0;
|
|
8302
|
-
}
|
|
8764
|
+
this.#viewState.invalidate();
|
|
8303
8765
|
}
|
|
8304
8766
|
#canSelect(node) {
|
|
8305
8767
|
if (this.selectionFilter === `none`) return false;
|
|
@@ -8312,6 +8774,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8312
8774
|
this.selectedNode = void 0;
|
|
8313
8775
|
}
|
|
8314
8776
|
#dispatchSelect(previous) {
|
|
8777
|
+
this.#viewState.selectionChanged();
|
|
8315
8778
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
8316
8779
|
detail: {
|
|
8317
8780
|
selected: new Set(this._selectedNodes),
|
|
@@ -8339,6 +8802,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8339
8802
|
this.requestUpdate();
|
|
8340
8803
|
}
|
|
8341
8804
|
clearSelection() {
|
|
8805
|
+
this.#viewState.clearPendingSelection();
|
|
8342
8806
|
if (this._selectedNodes.size === 0) return;
|
|
8343
8807
|
const prev = new Set(this._selectedNodes);
|
|
8344
8808
|
this.#clearSelectionInternal();
|
|
@@ -8384,6 +8848,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8384
8848
|
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
8385
8849
|
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
8386
8850
|
this._focusId = targetId;
|
|
8851
|
+
this.#viewState.centred(targetId);
|
|
8387
8852
|
this.#gestures.cancel();
|
|
8388
8853
|
this._previewId = void 0;
|
|
8389
8854
|
this.#animateTo(fromView);
|
|
@@ -8452,7 +8917,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8452
8917
|
#focusAndMaybeSelect(node) {
|
|
8453
8918
|
this.focusOn(node);
|
|
8454
8919
|
const id = this.#idOf(node);
|
|
8455
|
-
if (id !== void 0) this
|
|
8920
|
+
if (id !== void 0) this.#setKeyboardCursor(id, node);
|
|
8456
8921
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
8457
8922
|
}
|
|
8458
8923
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -8490,10 +8955,15 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8490
8955
|
const id = node ? this.#idOf(node) : void 0;
|
|
8491
8956
|
if (!node || id === void 0) return false;
|
|
8492
8957
|
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
8493
|
-
this
|
|
8958
|
+
this.#setKeyboardCursor(id, node);
|
|
8494
8959
|
this.requestUpdate();
|
|
8495
8960
|
return true;
|
|
8496
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
|
+
}
|
|
8497
8967
|
/**
|
|
8498
8968
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
8499
8969
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -8506,6 +8976,49 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8506
8976
|
depth: n.depth
|
|
8507
8977
|
}));
|
|
8508
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
|
+
}
|
|
8509
9022
|
#matchesFilter(node, depth) {
|
|
8510
9023
|
if (!this.filterPredicate) return true;
|
|
8511
9024
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -8791,6 +9304,7 @@ __decorate([property({
|
|
|
8791
9304
|
reflect: true
|
|
8792
9305
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
8793
9306
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
9307
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "restoreLoads", void 0);
|
|
8794
9308
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
8795
9309
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
8796
9310
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
@@ -8825,6 +9339,30 @@ function findIciclePath(root, key) {
|
|
|
8825
9339
|
}
|
|
8826
9340
|
}
|
|
8827
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
|
+
/**
|
|
8828
9366
|
* Compute the visible rectangles for an icicle view.
|
|
8829
9367
|
*
|
|
8830
9368
|
* Bands start at the zoom node's **children** (the zoom node itself is shown in
|
|
@@ -8878,6 +9416,259 @@ function computeIcicleLayout(root, options = {}) {
|
|
|
8878
9416
|
return rects;
|
|
8879
9417
|
}
|
|
8880
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
|
|
8881
9672
|
//#region src/icicle-tree/icicletree-list.ts
|
|
8882
9673
|
let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
8883
9674
|
constructor(..._args) {
|
|
@@ -8895,6 +9686,14 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8895
9686
|
this.disableKeyboardNavigation = false;
|
|
8896
9687
|
this.registry = void 0;
|
|
8897
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 = [];
|
|
8898
9697
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8899
9698
|
labelSelector: `.icicle-label`,
|
|
8900
9699
|
highlightKey: `incr-search-highlight`
|
|
@@ -8923,6 +9722,9 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8923
9722
|
});
|
|
8924
9723
|
this.#pendingKeys = /* @__PURE__ */ new Set();
|
|
8925
9724
|
this.#aborters = /* @__PURE__ */ new Map();
|
|
9725
|
+
this.#onScroll = () => {
|
|
9726
|
+
if (!this.#viewState.pendingScroll) this.#viewState.notify();
|
|
9727
|
+
};
|
|
8926
9728
|
}
|
|
8927
9729
|
static {
|
|
8928
9730
|
this.shadowRootOptions = {
|
|
@@ -8938,6 +9740,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8938
9740
|
if (this._zoomKey === void 0) return void 0;
|
|
8939
9741
|
return findIcicleNode(this.#effectiveRoot, this._zoomKey);
|
|
8940
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;
|
|
8941
9753
|
#modelUnsubscribe;
|
|
8942
9754
|
#pendingKeys;
|
|
8943
9755
|
#aborters;
|
|
@@ -8974,6 +9786,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8974
9786
|
});
|
|
8975
9787
|
this.registry = registry;
|
|
8976
9788
|
this.#syncSearchCommands();
|
|
9789
|
+
this.#viewState.observeScrollTarget(this);
|
|
9790
|
+
this.addEventListener(`scroll`, this.#onScroll, { passive: true });
|
|
8977
9791
|
if (typeof ResizeObserver !== `undefined`) {
|
|
8978
9792
|
this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
|
|
8979
9793
|
this.#resizeObserver.observe(this);
|
|
@@ -8986,6 +9800,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8986
9800
|
for (const aborter of this.#aborters.values()) aborter.abort();
|
|
8987
9801
|
this.#aborters.clear();
|
|
8988
9802
|
this.#pendingKeys.clear();
|
|
9803
|
+
this.removeEventListener(`scroll`, this.#onScroll);
|
|
8989
9804
|
this.#resizeObserver?.disconnect();
|
|
8990
9805
|
this.#resizeObserver = void 0;
|
|
8991
9806
|
if (this.#ownedRegistry) {
|
|
@@ -9007,28 +9822,23 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9007
9822
|
this.#unregisterSearchCommands?.();
|
|
9008
9823
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
9009
9824
|
}
|
|
9010
|
-
|
|
9011
|
-
if (changedProps.has(`root`)
|
|
9012
|
-
if (changedProps.has(`
|
|
9013
|
-
this.#modelUnsubscribe?.();
|
|
9014
|
-
this.#modelUnsubscribe = void 0;
|
|
9015
|
-
if (this.model) {
|
|
9016
|
-
this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
9017
|
-
if (event.type === `root`) {
|
|
9018
|
-
this.#resetViewState();
|
|
9019
|
-
this.requestUpdate();
|
|
9020
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
9021
|
-
});
|
|
9022
|
-
this.#resetViewState();
|
|
9023
|
-
}
|
|
9024
|
-
}
|
|
9025
|
-
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) {
|
|
9026
9828
|
this.#anchorIndex = void 0;
|
|
9027
9829
|
this.clearSelection();
|
|
9028
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
|
+
}
|
|
9029
9838
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
9030
|
-
if (this._zoomKey !== void 0 && !findIcicleNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
9031
9839
|
this.#ensureVisibleChildren();
|
|
9840
|
+
this.#continueRestoreLoads();
|
|
9841
|
+
this.#viewState.applyPendingScroll((scroll) => applyHostScroll(this, scroll));
|
|
9032
9842
|
this.#syncTooltip();
|
|
9033
9843
|
}
|
|
9034
9844
|
/** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
|
|
@@ -9078,12 +9888,103 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9078
9888
|
popover.hidePopover();
|
|
9079
9889
|
} catch {}
|
|
9080
9890
|
}
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
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
|
+
}
|
|
9086
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;
|
|
9087
9988
|
#ensureVisibleChildren() {
|
|
9088
9989
|
if (!this.model && !this.loadChildren) return;
|
|
9089
9990
|
for (const rect of this.#rects()) {
|
|
@@ -9127,6 +10028,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9127
10028
|
this.selectedNode = void 0;
|
|
9128
10029
|
}
|
|
9129
10030
|
#dispatchSelect(previous) {
|
|
10031
|
+
if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
|
|
10032
|
+
this.#viewState.notify();
|
|
9130
10033
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
9131
10034
|
detail: {
|
|
9132
10035
|
selected: new Set(this._selectedNodes),
|
|
@@ -9154,6 +10057,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9154
10057
|
this.requestUpdate();
|
|
9155
10058
|
}
|
|
9156
10059
|
clearSelection() {
|
|
10060
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9157
10061
|
if (this._selectedNodes.size === 0) return;
|
|
9158
10062
|
const prev = new Set(this._selectedNodes);
|
|
9159
10063
|
this.#clearSelectionInternal();
|
|
@@ -9190,11 +10094,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9190
10094
|
* Fires `zoom` and the semantic `expand` event.
|
|
9191
10095
|
*/
|
|
9192
10096
|
zoomTo(node) {
|
|
10097
|
+
this.#zoomChanged();
|
|
9193
10098
|
if (this._zoomKey === node.item.key) return;
|
|
9194
10099
|
this._zoomKey = node.item.key;
|
|
9195
10100
|
this.tickled.clearTickled();
|
|
9196
|
-
const
|
|
9197
|
-
|
|
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;
|
|
9198
10104
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9199
10105
|
detail: {
|
|
9200
10106
|
node,
|
|
@@ -9218,11 +10124,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9218
10124
|
* the semantic `collapse` event for the previous zoom root.
|
|
9219
10125
|
*/
|
|
9220
10126
|
zoomOut() {
|
|
10127
|
+
this.#zoomChanged();
|
|
9221
10128
|
if (this._zoomKey === void 0) return;
|
|
9222
10129
|
const previous = this.focusedNode;
|
|
9223
10130
|
const path = this._zoomKey === void 0 ? void 0 : findIciclePath(this.#effectiveRoot, this._zoomKey);
|
|
9224
10131
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
9225
10132
|
this._zoomKey = void 0;
|
|
10133
|
+
this.#zoomPath = [];
|
|
9226
10134
|
this.tickled.clearTickled();
|
|
9227
10135
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9228
10136
|
detail: {
|
|
@@ -9242,6 +10150,11 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9242
10150
|
}));
|
|
9243
10151
|
this.requestUpdate();
|
|
9244
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
|
+
}
|
|
9245
10158
|
#canZoom(node) {
|
|
9246
10159
|
if (node.item.isLeaf === true) return false;
|
|
9247
10160
|
if (node.children !== void 0) return node.children.length > 0;
|
|
@@ -9277,7 +10190,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9277
10190
|
navigateToNode(predicate) {
|
|
9278
10191
|
const root = this.#effectiveRoot;
|
|
9279
10192
|
if (!root?.children) return false;
|
|
9280
|
-
const keyPath =
|
|
10193
|
+
const keyPath = findIcicleKeyPath(root.children, predicate);
|
|
9281
10194
|
if (!keyPath) return false;
|
|
9282
10195
|
return this.navigateTo(keyPath);
|
|
9283
10196
|
}
|
|
@@ -9288,35 +10201,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9288
10201
|
revealKey(key) {
|
|
9289
10202
|
const root = this.#effectiveRoot;
|
|
9290
10203
|
if (!root?.children) return false;
|
|
9291
|
-
const path =
|
|
10204
|
+
const path = findIcicleKeyPath(root.children, (n) => n.item.key === key);
|
|
9292
10205
|
if (!path) return false;
|
|
9293
10206
|
if (path.length > this.maxDepth) {
|
|
9294
|
-
const ancestor = this.#
|
|
10207
|
+
const ancestor = resolveIcicleKeyPath(this.#effectiveRoot, path.slice(0, path.length - this.maxDepth));
|
|
9295
10208
|
if (ancestor) this.zoomTo(ancestor);
|
|
9296
10209
|
}
|
|
9297
10210
|
this.tickled.setTickled(key);
|
|
9298
10211
|
this.requestUpdate();
|
|
9299
10212
|
return true;
|
|
9300
10213
|
}
|
|
9301
|
-
#resolveKeyPath(keys) {
|
|
9302
|
-
let current = this.#effectiveRoot;
|
|
9303
|
-
for (const key of keys) {
|
|
9304
|
-
const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
|
|
9305
|
-
if (!found) return void 0;
|
|
9306
|
-
current = found;
|
|
9307
|
-
}
|
|
9308
|
-
return current;
|
|
9309
|
-
}
|
|
9310
|
-
#findKeyPath(nodes, predicate, prefix = []) {
|
|
9311
|
-
for (const node of nodes) {
|
|
9312
|
-
const path = [...prefix, node.item.key];
|
|
9313
|
-
if (predicate(node)) return path;
|
|
9314
|
-
if (node.children?.length) {
|
|
9315
|
-
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
9316
|
-
if (hit) return hit;
|
|
9317
|
-
}
|
|
9318
|
-
}
|
|
9319
|
-
}
|
|
9320
10214
|
/**
|
|
9321
10215
|
* All nodes currently visible as rectangles, in render (band-major) order,
|
|
9322
10216
|
* with absolute depth (root's children are depth 0).
|
|
@@ -9393,44 +10287,20 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9393
10287
|
const currentIndex = tickledKey === void 0 ? -1 : rects.findIndex((r) => r.node.item.key === tickledKey);
|
|
9394
10288
|
const current = currentIndex >= 0 ? rects[currentIndex] : void 0;
|
|
9395
10289
|
const moveTo = (rect) => {
|
|
9396
|
-
if (!rect) return;
|
|
9397
10290
|
event.preventDefault();
|
|
9398
10291
|
this.tickled.setTickled(rect.node.item.key);
|
|
10292
|
+
this.#keyboardCursor = true;
|
|
10293
|
+
this.#pendingFocus = void 0;
|
|
10294
|
+
this.#viewState.notify();
|
|
9399
10295
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
9400
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
|
+
}
|
|
9401
10303
|
switch (event.key) {
|
|
9402
|
-
case `ArrowLeft`:
|
|
9403
|
-
case `ArrowRight`: {
|
|
9404
|
-
event.preventDefault();
|
|
9405
|
-
const dir = event.key === `ArrowRight` ? 1 : -1;
|
|
9406
|
-
if (!current) {
|
|
9407
|
-
moveTo(rects[0]);
|
|
9408
|
-
return;
|
|
9409
|
-
}
|
|
9410
|
-
const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
|
|
9411
|
-
moveTo(siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1)));
|
|
9412
|
-
return;
|
|
9413
|
-
}
|
|
9414
|
-
case `ArrowUp`:
|
|
9415
|
-
if (!current) {
|
|
9416
|
-
moveTo(rects[0]);
|
|
9417
|
-
return;
|
|
9418
|
-
}
|
|
9419
|
-
moveTo((current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0]);
|
|
9420
|
-
return;
|
|
9421
|
-
case `ArrowDown`:
|
|
9422
|
-
if (!current) {
|
|
9423
|
-
moveTo(rects[0]);
|
|
9424
|
-
return;
|
|
9425
|
-
}
|
|
9426
|
-
moveTo(rects.find((r) => r.parentKey === current.node.item.key));
|
|
9427
|
-
return;
|
|
9428
|
-
case `Home`:
|
|
9429
|
-
moveTo(rects[0]);
|
|
9430
|
-
return;
|
|
9431
|
-
case `End`:
|
|
9432
|
-
moveTo(rects.at(-1));
|
|
9433
|
-
return;
|
|
9434
10304
|
case `Enter`:
|
|
9435
10305
|
if (!current) return;
|
|
9436
10306
|
event.preventDefault();
|
|
@@ -9480,6 +10350,10 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9480
10350
|
if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
|
|
9481
10351
|
else this.zoomTo(node);
|
|
9482
10352
|
}
|
|
10353
|
+
#onRectPointerEnter(key) {
|
|
10354
|
+
this.tickled.handlePointerEnter(key);
|
|
10355
|
+
if (this.tickled.tickledId === key) this.#keyboardCursor = false;
|
|
10356
|
+
}
|
|
9483
10357
|
#renderRect(rect) {
|
|
9484
10358
|
const { node } = rect;
|
|
9485
10359
|
const selected = this._selectedNodes.has(node);
|
|
@@ -9508,7 +10382,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9508
10382
|
style=${sizeStyle}
|
|
9509
10383
|
@click=${(e) => this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e)}
|
|
9510
10384
|
@dblclick=${() => this.#handleItemDblClick(rect)}
|
|
9511
|
-
@pointerenter=${() => this
|
|
10385
|
+
@pointerenter=${() => this.#onRectPointerEnter(node.item.key)}
|
|
9512
10386
|
>
|
|
9513
10387
|
<span class="icicle-label ${hideLabel ? `hide-label` : ``} ${rotated ? `label-vertical` : ``}" part="label">${node.item.label}</span>
|
|
9514
10388
|
</div>
|
|
@@ -9553,185 +10427,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9553
10427
|
tickledStyles,
|
|
9554
10428
|
tickledItemStyles,
|
|
9555
10429
|
listSearchStyles,
|
|
9556
|
-
|
|
9557
|
-
:host {
|
|
9558
|
-
--icicletree-band-height: 44px;
|
|
9559
|
-
--icicletree-band-gap: 2px;
|
|
9560
|
-
--icicletree-rect-gap: 2px;
|
|
9561
|
-
--icicletree-rect-bg: var(--surface-4);
|
|
9562
|
-
--icicletree-rect-text: var(--surface-4-text);
|
|
9563
|
-
--icicletree-font-size: var(--text-s);
|
|
9564
|
-
|
|
9565
|
-
display: block;
|
|
9566
|
-
overflow: auto;
|
|
9567
|
-
}
|
|
9568
|
-
|
|
9569
|
-
.icicle {
|
|
9570
|
-
display: flex;
|
|
9571
|
-
gap: var(--icicletree-band-gap);
|
|
9572
|
-
outline: none;
|
|
9573
|
-
}
|
|
9574
|
-
|
|
9575
|
-
.icicle.horizontal {
|
|
9576
|
-
flex-direction: column;
|
|
9577
|
-
}
|
|
9578
|
-
|
|
9579
|
-
.icicle.vertical {
|
|
9580
|
-
flex-direction: row;
|
|
9581
|
-
min-height: 200px;
|
|
9582
|
-
}
|
|
9583
|
-
|
|
9584
|
-
.breadcrumb {
|
|
9585
|
-
display: flex;
|
|
9586
|
-
align-items: center;
|
|
9587
|
-
padding: var(--space-xs) var(--space-s);
|
|
9588
|
-
background: var(--surface-3);
|
|
9589
|
-
color: var(--surface-3-text);
|
|
9590
|
-
border: var(--border-subtle);
|
|
9591
|
-
border-radius: var(--radius-m);
|
|
9592
|
-
font-size: var(--text-s);
|
|
9593
|
-
flex-shrink: 0;
|
|
9594
|
-
}
|
|
9595
|
-
|
|
9596
|
-
.breadcrumb ixfx-crumb-navigation {
|
|
9597
|
-
flex: 1;
|
|
9598
|
-
min-width: 0;
|
|
9599
|
-
}
|
|
9600
|
-
|
|
9601
|
-
.band {
|
|
9602
|
-
position: relative;
|
|
9603
|
-
}
|
|
9604
|
-
|
|
9605
|
-
.icicle.horizontal .band {
|
|
9606
|
-
height: var(--icicletree-band-height);
|
|
9607
|
-
}
|
|
9608
|
-
|
|
9609
|
-
.icicle.vertical .band {
|
|
9610
|
-
flex: 1 1 0;
|
|
9611
|
-
min-width: 0;
|
|
9612
|
-
height: 100%;
|
|
9613
|
-
}
|
|
9614
|
-
|
|
9615
|
-
.rect {
|
|
9616
|
-
box-sizing: border-box;
|
|
9617
|
-
position: absolute;
|
|
9618
|
-
overflow: hidden;
|
|
9619
|
-
display: flex;
|
|
9620
|
-
align-items: center;
|
|
9621
|
-
padding: 0 var(--space-s);
|
|
9622
|
-
background: var(--icicletree-rect-bg);
|
|
9623
|
-
color: var(--icicletree-rect-text);
|
|
9624
|
-
font-size: var(--icicletree-font-size);
|
|
9625
|
-
cursor: pointer;
|
|
9626
|
-
transition:
|
|
9627
|
-
width var(--transition),
|
|
9628
|
-
height var(--transition),
|
|
9629
|
-
left var(--transition),
|
|
9630
|
-
top var(--transition),
|
|
9631
|
-
opacity var(--transition),
|
|
9632
|
-
background-color var(--transition);
|
|
9633
|
-
}
|
|
9634
|
-
|
|
9635
|
-
.icicle.horizontal .rect {
|
|
9636
|
-
top: 0;
|
|
9637
|
-
height: 100%;
|
|
9638
|
-
}
|
|
9639
|
-
|
|
9640
|
-
/*
|
|
9641
|
-
* A rotated label needs its full allotted width to be usable — the
|
|
9642
|
-
* default left/right padding (--space-s, ~4px a side) can consume
|
|
9643
|
-
* most or all of a rect sized down to minVerticalLabelWidth, leaving
|
|
9644
|
-
* the rotated text clipped to invisible. #labelMode's width estimate
|
|
9645
|
-
* assumes the whole rect width is usable, so drop the padding here to
|
|
9646
|
-
* match that assumption instead of also budgeting for it in the
|
|
9647
|
-
* threshold (which would make already-narrow rects need even more).
|
|
9648
|
-
*/
|
|
9649
|
-
.icicle.horizontal .rect.rotated {
|
|
9650
|
-
padding: 0;
|
|
9651
|
-
}
|
|
9652
|
-
|
|
9653
|
-
.icicle.vertical .rect {
|
|
9654
|
-
left: 0;
|
|
9655
|
-
width: 100%;
|
|
9656
|
-
padding: var(--space-s) 0;
|
|
9657
|
-
justify-content: center;
|
|
9658
|
-
writing-mode: horizontal-tb;
|
|
9659
|
-
}
|
|
9660
|
-
|
|
9661
|
-
.rect.filtered {
|
|
9662
|
-
opacity: 0.3;
|
|
9663
|
-
filter: saturate(0.4);
|
|
9664
|
-
}
|
|
9665
|
-
|
|
9666
|
-
.rect.empty {
|
|
9667
|
-
padding: 0;
|
|
9668
|
-
border: none;
|
|
9669
|
-
}
|
|
9670
|
-
|
|
9671
|
-
.rect.loading {
|
|
9672
|
-
cursor: progress;
|
|
9673
|
-
opacity: 0.6;
|
|
9674
|
-
}
|
|
9675
|
-
|
|
9676
|
-
.icicle-label {
|
|
9677
|
-
overflow: hidden;
|
|
9678
|
-
text-overflow: ellipsis;
|
|
9679
|
-
white-space: nowrap;
|
|
9680
|
-
flex: 1;
|
|
9681
|
-
min-width: 0;
|
|
9682
|
-
}
|
|
9683
|
-
|
|
9684
|
-
.icicle-label.hide-label {
|
|
9685
|
-
visibility: hidden;
|
|
9686
|
-
}
|
|
9687
|
-
|
|
9688
|
-
.icicle-label.label-vertical {
|
|
9689
|
-
writing-mode: vertical-rl;
|
|
9690
|
-
text-orientation: sideways;
|
|
9691
|
-
flex: none;
|
|
9692
|
-
width: 100%;
|
|
9693
|
-
height: 100%;
|
|
9694
|
-
padding: var(--space-2xs, 2px) 0;
|
|
9695
|
-
box-sizing: border-box;
|
|
9696
|
-
display: flex;
|
|
9697
|
-
align-items: center;
|
|
9698
|
-
justify-content: center;
|
|
9699
|
-
}
|
|
9700
|
-
|
|
9701
|
-
/*
|
|
9702
|
-
* Instant tooltip for rects whose label is rotated or hidden — anchored
|
|
9703
|
-
* via CSS anchor positioning (anchor-name set on the tickled rect in
|
|
9704
|
-
* #renderRect) rather than a JS-computed position, and shown/hidden via
|
|
9705
|
-
* the Popover API rather than the browser's own (slow, un-stylable)
|
|
9706
|
-
* title tooltip.
|
|
9707
|
-
*/
|
|
9708
|
-
.icicle-tooltip {
|
|
9709
|
-
position: fixed;
|
|
9710
|
-
position-anchor: --icicle-rect-anchor;
|
|
9711
|
-
position-area: bottom center;
|
|
9712
|
-
position-try-fallbacks: top center;
|
|
9713
|
-
margin: var(--space-xs, 4px) 0 0;
|
|
9714
|
-
padding: var(--space-2xs, 2px) var(--space-xs, 6px);
|
|
9715
|
-
background: var(--surface-5);
|
|
9716
|
-
color: var(--surface-5-text);
|
|
9717
|
-
border: var(--border-subtle);
|
|
9718
|
-
border-radius: var(--radius-s, 4px);
|
|
9719
|
-
font-size: var(--text-s);
|
|
9720
|
-
white-space: nowrap;
|
|
9721
|
-
pointer-events: none;
|
|
9722
|
-
inset: auto;
|
|
9723
|
-
}
|
|
9724
|
-
|
|
9725
|
-
.icicle-tooltip:popover-open {
|
|
9726
|
-
display: block;
|
|
9727
|
-
}
|
|
9728
|
-
|
|
9729
|
-
.empty-state {
|
|
9730
|
-
padding: var(--space-l);
|
|
9731
|
-
color: var(--surface-muted-text);
|
|
9732
|
-
font-size: var(--text-s);
|
|
9733
|
-
}
|
|
9734
|
-
`
|
|
10430
|
+
icicleTreeStyles
|
|
9735
10431
|
];
|
|
9736
10432
|
}
|
|
9737
10433
|
};
|
|
@@ -9786,6 +10482,7 @@ __decorate([property({
|
|
|
9786
10482
|
reflect: true
|
|
9787
10483
|
})], IcicleTreeListElement.prototype, "searchModes", void 0);
|
|
9788
10484
|
__decorate([state()], IcicleTreeListElement.prototype, "_zoomKey", void 0);
|
|
10485
|
+
__decorate([property({ attribute: false })], IcicleTreeListElement.prototype, "restoreLoads", void 0);
|
|
9789
10486
|
__decorate([query(`#icicle-tooltip`)], IcicleTreeListElement.prototype, "_tooltipEl", void 0);
|
|
9790
10487
|
IcicleTreeListElement = __decorate([safeCustomElement(`ixfx-icicletree-list`)], IcicleTreeListElement);
|
|
9791
10488
|
//#endregion
|
|
@@ -9811,6 +10508,136 @@ function findLeafCategory(categories, key) {
|
|
|
9811
10508
|
return flattenCategories(categories).find((c) => c.key === key);
|
|
9812
10509
|
}
|
|
9813
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
|
|
9814
10641
|
//#region src/multicol-list/types.ts
|
|
9815
10642
|
const ALL_KEY = `__all__`;
|
|
9816
10643
|
//#endregion
|
|
@@ -9862,6 +10689,7 @@ var MulticolColumnController = class {
|
|
|
9862
10689
|
#dataProvider;
|
|
9863
10690
|
#onSelectionChange;
|
|
9864
10691
|
#onRequestData;
|
|
10692
|
+
#onScroll;
|
|
9865
10693
|
#abortController = void 0;
|
|
9866
10694
|
#host;
|
|
9867
10695
|
#allRow;
|
|
@@ -9869,7 +10697,19 @@ var MulticolColumnController = class {
|
|
|
9869
10697
|
#selectedValues = [];
|
|
9870
10698
|
#allSelected = true;
|
|
9871
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;
|
|
9872
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;
|
|
9873
10713
|
#state;
|
|
9874
10714
|
constructor(options) {
|
|
9875
10715
|
this.id = options.id;
|
|
@@ -9879,6 +10719,7 @@ var MulticolColumnController = class {
|
|
|
9879
10719
|
this.#dataProvider = options.dataProvider;
|
|
9880
10720
|
this.#onSelectionChange = options.onSelectionChange;
|
|
9881
10721
|
this.#onRequestData = options.onRequestData;
|
|
10722
|
+
this.#onScroll = options.onScroll;
|
|
9882
10723
|
this.#state = {
|
|
9883
10724
|
id: options.id,
|
|
9884
10725
|
categoryKey: options.category.key,
|
|
@@ -9954,7 +10795,10 @@ var MulticolColumnController = class {
|
|
|
9954
10795
|
grouped: category.defaults?.grouped ?? false,
|
|
9955
10796
|
menu: category.defaults?.menu ?? true
|
|
9956
10797
|
};
|
|
9957
|
-
this
|
|
10798
|
+
this.#pendingSelection = EMPTY_KEYS;
|
|
10799
|
+
this.#pendingScroll = void 0;
|
|
10800
|
+
this.#itemKeys = EMPTY_KEYS;
|
|
10801
|
+
this.#setAllSelection(`data`);
|
|
9958
10802
|
this.#syncGrouping();
|
|
9959
10803
|
}
|
|
9960
10804
|
get listerHost() {
|
|
@@ -9979,7 +10823,7 @@ var MulticolColumnController = class {
|
|
|
9979
10823
|
allRow.appendChild(allLabel);
|
|
9980
10824
|
const applyAll = () => {
|
|
9981
10825
|
if (this.#allSelected) return;
|
|
9982
|
-
this.#setAllSelection();
|
|
10826
|
+
this.#setAllSelection(`user`);
|
|
9983
10827
|
};
|
|
9984
10828
|
allRow.addEventListener(`click`, applyAll);
|
|
9985
10829
|
allRow.addEventListener(`keydown`, (e) => {
|
|
@@ -10000,6 +10844,10 @@ var MulticolColumnController = class {
|
|
|
10000
10844
|
this.#syncGrouping();
|
|
10001
10845
|
this.#syncAllRow();
|
|
10002
10846
|
host.append(allRow, lister);
|
|
10847
|
+
host.addEventListener(`scroll`, this.#handleScroll, { passive: true });
|
|
10848
|
+
this.#disposeScrollIntent = onUserScrollIntent(host, () => {
|
|
10849
|
+
this.#pendingScroll = void 0;
|
|
10850
|
+
});
|
|
10003
10851
|
if (this.#lastItems.length > 0) {
|
|
10004
10852
|
this.#pushItems();
|
|
10005
10853
|
this.#restoreAfterSync();
|
|
@@ -10007,6 +10855,9 @@ var MulticolColumnController = class {
|
|
|
10007
10855
|
}
|
|
10008
10856
|
unmount() {
|
|
10009
10857
|
this.#abortPending();
|
|
10858
|
+
this.#host?.removeEventListener(`scroll`, this.#handleScroll);
|
|
10859
|
+
this.#disposeScrollIntent?.();
|
|
10860
|
+
this.#disposeScrollIntent = void 0;
|
|
10010
10861
|
this.#allRow?.remove();
|
|
10011
10862
|
this.#listerEl?.remove();
|
|
10012
10863
|
this.#allRow = void 0;
|
|
@@ -10029,17 +10880,17 @@ var MulticolColumnController = class {
|
|
|
10029
10880
|
}
|
|
10030
10881
|
#applyItems(items) {
|
|
10031
10882
|
this.#lastItems = items;
|
|
10032
|
-
const
|
|
10033
|
-
|
|
10034
|
-
this.#selectedValues
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
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();
|
|
10039
10892
|
}
|
|
10040
|
-
this.#
|
|
10041
|
-
this.#restoreAfterSync();
|
|
10042
|
-
if (changed) this.#onSelectionChange(this.id, valid);
|
|
10893
|
+
if (r.changed) this.#onSelectionChange(this.id, r.selected, r.restored ? `restore` : `data`);
|
|
10043
10894
|
}
|
|
10044
10895
|
#pushItems() {
|
|
10045
10896
|
if (!this.#listerEl) return;
|
|
@@ -10064,7 +10915,7 @@ var MulticolColumnController = class {
|
|
|
10064
10915
|
return this.#listerEl.querySelectorAll(`:scope > .ungrouped-host > *, :scope > ixfx-panel > *`);
|
|
10065
10916
|
}
|
|
10066
10917
|
#handleSelect = () => {
|
|
10067
|
-
if (this.#syncingSelection) return;
|
|
10918
|
+
if (this.#syncingSelection || this.#syncsInFlight > 0) return;
|
|
10068
10919
|
const values = [...this.#listerEl?.selectedItems ?? []].map((i) => i.key).filter((k) => k !== ALL_KEY);
|
|
10069
10920
|
if (values.length === this.#selectedValues.length && values.every((v) => this.#selectedValues.includes(v))) {
|
|
10070
10921
|
this.#syncAllRow();
|
|
@@ -10073,9 +10924,9 @@ var MulticolColumnController = class {
|
|
|
10073
10924
|
this.#selectedValues = values;
|
|
10074
10925
|
this.#allSelected = values.length === 0;
|
|
10075
10926
|
this.#syncAllRow();
|
|
10076
|
-
this.#onSelectionChange(this.id, values);
|
|
10927
|
+
this.#onSelectionChange(this.id, values, `user`);
|
|
10077
10928
|
};
|
|
10078
|
-
#setAllSelection() {
|
|
10929
|
+
#setAllSelection(origin) {
|
|
10079
10930
|
const previous = this.#selectedValues;
|
|
10080
10931
|
this.#syncingSelection = true;
|
|
10081
10932
|
try {
|
|
@@ -10086,17 +10937,64 @@ var MulticolColumnController = class {
|
|
|
10086
10937
|
this.#syncingSelection = false;
|
|
10087
10938
|
}
|
|
10088
10939
|
this.#syncAllRow();
|
|
10089
|
-
if (previous.length > 0) this.#onSelectionChange(this.id, []);
|
|
10940
|
+
if (previous.length > 0) this.#onSelectionChange(this.id, [], origin);
|
|
10090
10941
|
}
|
|
10942
|
+
/** Back to 'All' — an explicit selection change. */
|
|
10091
10943
|
clearSelection() {
|
|
10092
|
-
this.#setAllSelection();
|
|
10944
|
+
this.#setAllSelection(`user`);
|
|
10093
10945
|
}
|
|
10094
10946
|
get selectedValues() {
|
|
10095
10947
|
return this.#selectedValues;
|
|
10096
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
|
+
};
|
|
10097
10992
|
#restoreAfterSync() {
|
|
10098
10993
|
if (!this.#listerEl) return;
|
|
10099
|
-
this.#
|
|
10994
|
+
this.#syncsInFlight++;
|
|
10995
|
+
this.#listerEl.updateComplete.finally(() => {
|
|
10996
|
+
this.#syncsInFlight--;
|
|
10997
|
+
}).then(() => {
|
|
10100
10998
|
if (!this.#listerEl || !this.#host) return;
|
|
10101
10999
|
this.#syncingSelection = true;
|
|
10102
11000
|
try {
|
|
@@ -10109,6 +11007,7 @@ var MulticolColumnController = class {
|
|
|
10109
11007
|
this.#syncingSelection = false;
|
|
10110
11008
|
}
|
|
10111
11009
|
this.#syncAllRow();
|
|
11010
|
+
this.#tryPendingScroll();
|
|
10112
11011
|
});
|
|
10113
11012
|
}
|
|
10114
11013
|
#syncAllRow() {
|
|
@@ -10140,12 +11039,201 @@ function renderItem(item) {
|
|
|
10140
11039
|
}
|
|
10141
11040
|
return li;
|
|
10142
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
|
+
`;
|
|
10143
11234
|
//#endregion
|
|
10144
11235
|
//#region src/multicol-list/multicol-list.ts
|
|
10145
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>`);
|
|
10146
|
-
const DEFAULT_COLUMN_WIDTH = 220;
|
|
10147
|
-
const MIN_COLUMN_WIDTH = 80;
|
|
10148
|
-
const MAX_COLUMN_WIDTH = 480;
|
|
10149
11237
|
const DRAG_THRESHOLD = 5;
|
|
10150
11238
|
let MulticolListElement = class MulticolListElement extends LitElement {
|
|
10151
11239
|
constructor(..._args) {
|
|
@@ -10170,240 +11258,33 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10170
11258
|
ghost: null,
|
|
10171
11259
|
insert: -1
|
|
10172
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();
|
|
10173
11268
|
}
|
|
10174
11269
|
static {
|
|
10175
11270
|
this.styles = [
|
|
10176
11271
|
themeFallbacks,
|
|
10177
11272
|
handleStyles,
|
|
10178
|
-
|
|
10179
|
-
:host {
|
|
10180
|
-
--multicol-list-bg: var(--surface-3);
|
|
10181
|
-
--multicol-list-header-bg: var(--surface-2);
|
|
10182
|
-
--multicol-list-header-text: var(--surface-3-text);
|
|
10183
|
-
--multicol-list-border: var(--border);
|
|
10184
|
-
--multicol-list-min-column-width: ${MIN_COLUMN_WIDTH}px;
|
|
10185
|
-
--multicol-list-max-column-width: ${MAX_COLUMN_WIDTH}px;
|
|
10186
|
-
--multicol-list-header-font-size: unset;
|
|
10187
|
-
|
|
10188
|
-
display: block;
|
|
10189
|
-
overflow: hidden;
|
|
10190
|
-
/* A sideways trackpad swipe over the component must never pan it
|
|
10191
|
-
(or the page behind it, or trigger history navigation) */
|
|
10192
|
-
overscroll-behavior-x: none;
|
|
10193
|
-
background: var(--multicol-list-bg);
|
|
10194
|
-
color: var(--surface-3-text);
|
|
10195
|
-
font-family: var(--font-family);
|
|
10196
|
-
}
|
|
10197
|
-
#container {
|
|
10198
|
-
display: flex;
|
|
10199
|
-
flex-direction: row;
|
|
10200
|
-
align-items: stretch;
|
|
10201
|
-
overflow: hidden;
|
|
10202
|
-
overscroll-behavior-x: none;
|
|
10203
|
-
position: relative;
|
|
10204
|
-
height: 100%;
|
|
10205
|
-
}
|
|
10206
|
-
.column {
|
|
10207
|
-
flex-shrink: 0;
|
|
10208
|
-
display: flex;
|
|
10209
|
-
flex-direction: column;
|
|
10210
|
-
border-right: var(--multicol-list-border);
|
|
10211
|
-
min-width: var(--multicol-list-min-column-width);
|
|
10212
|
-
max-width: var(--multicol-list-max-column-width);
|
|
10213
|
-
background: var(--surface-3);
|
|
10214
|
-
}
|
|
10215
|
-
.column-header {
|
|
10216
|
-
display: flex;
|
|
10217
|
-
align-items: center;
|
|
10218
|
-
gap: var(--space-xs);
|
|
10219
|
-
height: 32px;
|
|
10220
|
-
background: var(--multicol-list-header-bg);
|
|
10221
|
-
color: var(--multicol-list-header-text);
|
|
10222
|
-
border-bottom: var(--border);
|
|
10223
|
-
position: relative;
|
|
10224
|
-
user-select: none;
|
|
10225
|
-
min-width: var(--multicol-list-min-column-width);
|
|
10226
|
-
box-sizing: border-box;
|
|
10227
|
-
}
|
|
10228
|
-
.column-header[data-dragging] {
|
|
10229
|
-
opacity: 0.4;
|
|
10230
|
-
}
|
|
10231
|
-
.reorder-handle {
|
|
10232
|
-
cursor: grab;
|
|
10233
|
-
opacity: 0;
|
|
10234
|
-
display: inline-flex;
|
|
10235
|
-
align-items: center;
|
|
10236
|
-
flex-shrink: 0;
|
|
10237
|
-
padding-left: var(--space-xs);
|
|
10238
|
-
touch-action: none;
|
|
10239
|
-
transition: opacity var(--transition, 0.15s ease-out);
|
|
10240
|
-
}
|
|
10241
|
-
.column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
|
|
10242
|
-
.reorder-handle:active {
|
|
10243
|
-
cursor: grabbing;
|
|
10244
|
-
}
|
|
10245
|
-
.reorder-handle .ixfx-handle {
|
|
10246
|
-
--ixfx-handle-length: 14px;
|
|
10247
|
-
--ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
|
|
10248
|
-
--ixfx-handle-dot-size: 3px;
|
|
10249
|
-
}
|
|
10250
|
-
.column-label {
|
|
10251
|
-
flex: 1;
|
|
10252
|
-
overflow: hidden;
|
|
10253
|
-
text-overflow: ellipsis;
|
|
10254
|
-
white-space: nowrap;
|
|
10255
|
-
padding: 0 var(--space-s);
|
|
10256
|
-
font-size: var(--multicol-list-header-font-size);
|
|
10257
|
-
font-weight: 600;
|
|
10258
|
-
background: transparent;
|
|
10259
|
-
border: none;
|
|
10260
|
-
color: inherit;
|
|
10261
|
-
cursor: pointer;
|
|
10262
|
-
text-align: left;
|
|
10263
|
-
min-width: 0;
|
|
10264
|
-
}
|
|
10265
|
-
.column-label:hover {
|
|
10266
|
-
text-decoration: underline;
|
|
10267
|
-
}
|
|
10268
|
-
.burger-btn {
|
|
10269
|
-
flex-shrink: 0;
|
|
10270
|
-
width: 28px;
|
|
10271
|
-
height: 28px;
|
|
10272
|
-
display: flex;
|
|
10273
|
-
align-items: center;
|
|
10274
|
-
justify-content: center;
|
|
10275
|
-
background: transparent;
|
|
10276
|
-
border: none;
|
|
10277
|
-
cursor: pointer;
|
|
10278
|
-
color: var(--surface-3-text);
|
|
10279
|
-
opacity: 0;
|
|
10280
|
-
transition: opacity var(--transition);
|
|
10281
|
-
border-radius: var(--radius-s);
|
|
10282
|
-
}
|
|
10283
|
-
.column:hover .burger-btn,
|
|
10284
|
-
.column-header:focus-within .burger-btn {
|
|
10285
|
-
opacity: 1;
|
|
10286
|
-
}
|
|
10287
|
-
.burger-btn:hover {
|
|
10288
|
-
background: var(--surface-4);
|
|
10289
|
-
}
|
|
10290
|
-
.resize-handle {
|
|
10291
|
-
position: absolute;
|
|
10292
|
-
right: -4px;
|
|
10293
|
-
top: 0;
|
|
10294
|
-
bottom: 0;
|
|
10295
|
-
width: 8px;
|
|
10296
|
-
cursor: col-resize;
|
|
10297
|
-
z-index: 1;
|
|
10298
|
-
display: flex;
|
|
10299
|
-
align-items: stretch;
|
|
10300
|
-
justify-content: center;
|
|
10301
|
-
touch-action: none;
|
|
10302
|
-
}
|
|
10303
|
-
.resize-handle::before {
|
|
10304
|
-
content: '';
|
|
10305
|
-
width: 2px;
|
|
10306
|
-
margin: 2px 0;
|
|
10307
|
-
border-radius: 1px;
|
|
10308
|
-
background: transparent;
|
|
10309
|
-
transition: background-color var(--transition);
|
|
10310
|
-
}
|
|
10311
|
-
.resize-handle:hover::before,
|
|
10312
|
-
.resize-handle[data-active]::before {
|
|
10313
|
-
background: var(--accent);
|
|
10314
|
-
}
|
|
10315
|
-
.lister-host {
|
|
10316
|
-
flex: 1;
|
|
10317
|
-
overflow-y: auto;
|
|
10318
|
-
overflow-x: hidden;
|
|
10319
|
-
/* Vertical scrolling only: horizontal deltas are swallowed here
|
|
10320
|
-
instead of chaining up and panning the whole component */
|
|
10321
|
-
overscroll-behavior-x: none;
|
|
10322
|
-
touch-action: pan-y;
|
|
10323
|
-
position: relative;
|
|
10324
|
-
display: flex;
|
|
10325
|
-
flex-direction: column;
|
|
10326
|
-
--list-gutter: 0;
|
|
10327
|
-
}
|
|
10328
|
-
.all-row {
|
|
10329
|
-
display: flex;
|
|
10330
|
-
align-items: center;
|
|
10331
|
-
padding: var(--space-xs) var(--space-s);
|
|
10332
|
-
cursor: pointer;
|
|
10333
|
-
user-select: none;
|
|
10334
|
-
min-height: 28px;
|
|
10335
|
-
box-sizing: border-box;
|
|
10336
|
-
outline: 1px solid transparent;
|
|
10337
|
-
background: var(--surface-3);
|
|
10338
|
-
}
|
|
10339
|
-
.all-row:hover,
|
|
10340
|
-
.all-row[data-selected] {
|
|
10341
|
-
background: var(--surface-5);
|
|
10342
|
-
}
|
|
10343
|
-
.all-row[data-selected] {
|
|
10344
|
-
outline-color: var(--accent);
|
|
10345
|
-
font-weight: 600;
|
|
10346
|
-
}
|
|
10347
|
-
ixfx-grouped-item-lister {
|
|
10348
|
-
display: block;
|
|
10349
|
-
}
|
|
10350
|
-
.drop-caret {
|
|
10351
|
-
position: absolute;
|
|
10352
|
-
top: 0;
|
|
10353
|
-
bottom: 0;
|
|
10354
|
-
width: 2px;
|
|
10355
|
-
background: var(--accent);
|
|
10356
|
-
border-radius: 1px;
|
|
10357
|
-
pointer-events: none;
|
|
10358
|
-
z-index: 10;
|
|
10359
|
-
}
|
|
10360
|
-
.item-label {
|
|
10361
|
-
flex: 1;
|
|
10362
|
-
}
|
|
10363
|
-
.count {
|
|
10364
|
-
opacity: 0.6;
|
|
10365
|
-
margin-left: var(--space-s);
|
|
10366
|
-
font-size: var(--text-s);
|
|
10367
|
-
}
|
|
10368
|
-
`
|
|
11273
|
+
multicolListStyles
|
|
10369
11274
|
];
|
|
10370
11275
|
}
|
|
10371
|
-
|
|
10372
|
-
|
|
10373
|
-
|
|
10374
|
-
|
|
10375
|
-
|
|
10376
|
-
|
|
10377
|
-
for (const colState of states) {
|
|
10378
|
-
const existing = existingMap.get(colState.id);
|
|
10379
|
-
if (existing) {
|
|
10380
|
-
if (existing.categoryKey !== colState.categoryKey) {
|
|
10381
|
-
const newCategory = findLeafCategory(this.categories, colState.categoryKey);
|
|
10382
|
-
if (newCategory) existing.setCategory(newCategory);
|
|
10383
|
-
}
|
|
10384
|
-
existing.updateState(colState);
|
|
10385
|
-
newColumns.push(existing);
|
|
10386
|
-
existingMap.delete(colState.id);
|
|
10387
|
-
continue;
|
|
10388
|
-
}
|
|
10389
|
-
const category = findLeafCategory(this.categories, colState.categoryKey);
|
|
10390
|
-
if (!category) continue;
|
|
10391
|
-
const controller = this.#createController(colState.id, category);
|
|
10392
|
-
controller.updateState(colState);
|
|
10393
|
-
newColumns.push(controller);
|
|
10394
|
-
}
|
|
10395
|
-
for (const col of existingMap.values()) col.unmount();
|
|
10396
|
-
this._columns = newColumns;
|
|
10397
|
-
this._updateColOffsets();
|
|
10398
|
-
this._emitColumnsChange(`add`);
|
|
10399
|
-
this._requeryAllColumns();
|
|
10400
|
-
this.requestUpdate();
|
|
10401
|
-
}
|
|
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;
|
|
10402
11282
|
addColumn(categoryKey, position) {
|
|
10403
11283
|
if (this.maxColumns > 0 && this._columns.length >= this.maxColumns) return;
|
|
10404
11284
|
const category = findLeafCategory(this.categories, categoryKey);
|
|
10405
11285
|
if (!category) return;
|
|
10406
|
-
|
|
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}`;
|
|
10407
11288
|
const controller = this.#createController(id, category);
|
|
10408
11289
|
if (position !== void 0 && position >= 0 && position < this._columns.length) this._columns.splice(position, 0, controller);
|
|
10409
11290
|
else this._columns.push(controller);
|
|
@@ -10431,9 +11312,15 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10431
11312
|
}));
|
|
10432
11313
|
}
|
|
10433
11314
|
clearSelection() {
|
|
11315
|
+
this.#dropPendingSelection();
|
|
10434
11316
|
for (const col of this._columns) col.clearSelection();
|
|
10435
11317
|
this.requestUpdate();
|
|
10436
11318
|
}
|
|
11319
|
+
createRenderRoot() {
|
|
11320
|
+
const root = super.createRenderRoot();
|
|
11321
|
+
root.addEventListener(VIEW_STATE_CHANGE, (e) => e.stopPropagation());
|
|
11322
|
+
return root;
|
|
11323
|
+
}
|
|
10437
11324
|
render() {
|
|
10438
11325
|
return html`
|
|
10439
11326
|
<div id="container">
|
|
@@ -10442,7 +11329,136 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10442
11329
|
</div>
|
|
10443
11330
|
`;
|
|
10444
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
|
+
}
|
|
10445
11460
|
updated(changedProperties) {
|
|
11461
|
+
if (changedProperties.has(`categories`)) this.#resolvePendingColumns();
|
|
10446
11462
|
const seedCategory = flattenCategories(this.categories)[0];
|
|
10447
11463
|
if (!this._autoSeeded && this._columns.length === 0 && seedCategory) {
|
|
10448
11464
|
this._autoSeeded = true;
|
|
@@ -10459,8 +11475,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10459
11475
|
this._updateColOffsets();
|
|
10460
11476
|
}
|
|
10461
11477
|
_renderColumn(col, idx) {
|
|
10462
|
-
const rawWidth = col.getState().width ??
|
|
10463
|
-
const width = Number.isFinite(rawWidth) ? rawWidth :
|
|
11478
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11479
|
+
const width = Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10464
11480
|
return html`
|
|
10465
11481
|
<div
|
|
10466
11482
|
class="column"
|
|
@@ -10522,7 +11538,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10522
11538
|
selectionMode: this.selectionMode,
|
|
10523
11539
|
dataProvider: this.dataProvider,
|
|
10524
11540
|
onSelectionChange: this._onColumnSelectionChange.bind(this),
|
|
10525
|
-
onRequestData: this._onColumnRequestData.bind(this)
|
|
11541
|
+
onRequestData: this._onColumnRequestData.bind(this),
|
|
11542
|
+
onScroll: () => this.#viewState.notify()
|
|
10526
11543
|
});
|
|
10527
11544
|
}
|
|
10528
11545
|
_onHeaderPointerDown(e, col, idx) {
|
|
@@ -10601,11 +11618,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10601
11618
|
e.preventDefault();
|
|
10602
11619
|
e.stopPropagation();
|
|
10603
11620
|
const startX = e.clientX;
|
|
10604
|
-
const rawStartW = col.getState().width ??
|
|
10605
|
-
const startW = Number.isFinite(rawStartW) ? rawStartW :
|
|
11621
|
+
const rawStartW = col.getState().width ?? 220;
|
|
11622
|
+
const startW = Number.isFinite(rawStartW) ? rawStartW : 220;
|
|
10606
11623
|
this._resizingColumnId = col.id;
|
|
10607
11624
|
const applyWidth = (clientX) => {
|
|
10608
|
-
const w = Math.max(
|
|
11625
|
+
const w = Math.max(80, Math.min(480, startW + clientX - startX));
|
|
10609
11626
|
col.setWidth(w);
|
|
10610
11627
|
return w;
|
|
10611
11628
|
};
|
|
@@ -10734,8 +11751,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10734
11751
|
this._requeryAllColumns();
|
|
10735
11752
|
this.requestUpdate();
|
|
10736
11753
|
}
|
|
10737
|
-
_onColumnSelectionChange(columnId, _values) {
|
|
11754
|
+
_onColumnSelectionChange(columnId, _values, origin) {
|
|
10738
11755
|
if (!this._columns.find((c) => c.id === columnId)) return;
|
|
11756
|
+
if (origin === `user`) this.#dropPendingSelection();
|
|
11757
|
+
this.#viewState.notify(origin);
|
|
10739
11758
|
for (const otherCol of this._columns) if (otherCol.id !== columnId) otherCol.requestData(this._buildQuery(otherCol));
|
|
10740
11759
|
const detail = {
|
|
10741
11760
|
columnId,
|
|
@@ -10770,22 +11789,23 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10770
11789
|
col.setCategory(leaves[0]);
|
|
10771
11790
|
changed = true;
|
|
10772
11791
|
}
|
|
10773
|
-
if (changed) this._emitColumnsChange(`category`);
|
|
11792
|
+
if (changed) this._emitColumnsChange(`category`, `data`);
|
|
10774
11793
|
}
|
|
10775
11794
|
_updateColOffsets() {
|
|
10776
11795
|
const offsets = [0];
|
|
10777
11796
|
let sum = 0;
|
|
10778
11797
|
for (const col of this._columns) {
|
|
10779
|
-
const rawWidth = col.getState().width ??
|
|
10780
|
-
sum += Number.isFinite(rawWidth) ? rawWidth :
|
|
11798
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11799
|
+
sum += Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10781
11800
|
offsets.push(sum);
|
|
10782
11801
|
}
|
|
10783
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;
|
|
10784
11803
|
}
|
|
10785
|
-
_emitColumnsChange(reason) {
|
|
11804
|
+
_emitColumnsChange(reason, viewStateReason = `user`) {
|
|
11805
|
+
this.#viewState.notify(viewStateReason);
|
|
10786
11806
|
this.dispatchEvent(new CustomEvent(`multicol-columns-change`, {
|
|
10787
11807
|
detail: {
|
|
10788
|
-
columns: this.
|
|
11808
|
+
columns: this._columns.map((c) => c.getState()),
|
|
10789
11809
|
reason
|
|
10790
11810
|
},
|
|
10791
11811
|
bubbles: true,
|
|
@@ -10795,8 +11815,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10795
11815
|
async autoSizeColumns(options) {
|
|
10796
11816
|
return new Promise((resolve) => {
|
|
10797
11817
|
setTimeout(() => {
|
|
10798
|
-
const minWidth = options?.minWidth ??
|
|
10799
|
-
const maxWidth = options?.maxWidth ??
|
|
11818
|
+
const minWidth = options?.minWidth ?? 80;
|
|
11819
|
+
const maxWidth = options?.maxWidth ?? 480;
|
|
10800
11820
|
const measure = (text) => {
|
|
10801
11821
|
const span = document.createElement(`span`);
|
|
10802
11822
|
span.style.position = `absolute`;
|
|
@@ -10874,6 +11894,11 @@ __decorate([state()], MulticolListElement.prototype, "_draggingColumnId", void 0
|
|
|
10874
11894
|
__decorate([state()], MulticolListElement.prototype, "_dropCaretX", void 0);
|
|
10875
11895
|
__decorate([query(`#container`)], MulticolListElement.prototype, "_containerEl", void 0);
|
|
10876
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
|
+
}
|
|
10877
11902
|
//#endregion
|
|
10878
11903
|
//#region src/notifier/pill-renderer.ts
|
|
10879
11904
|
const ROW_SPACING = 8;
|
|
@@ -14536,6 +15561,242 @@ function computeTreemapLayout(root, options = {}) {
|
|
|
14536
15561
|
return rects;
|
|
14537
15562
|
}
|
|
14538
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
|
|
14539
15800
|
//#region src/treemap/treemap-list.ts
|
|
14540
15801
|
let TreemapListElement = class TreemapListElement extends LitElement {
|
|
14541
15802
|
constructor(..._args) {
|
|
@@ -14557,6 +15818,33 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14557
15818
|
this.disableKeyboardNavigation = false;
|
|
14558
15819
|
this.registry = void 0;
|
|
14559
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
|
+
});
|
|
14560
15848
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
14561
15849
|
labelSelector: `.treemap-label`,
|
|
14562
15850
|
highlightKey: `incr-search-highlight`
|
|
@@ -14595,6 +15883,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14595
15883
|
if (this._zoomKey === void 0) return void 0;
|
|
14596
15884
|
return findTreemapNode(this.#effectiveRoot, this._zoomKey);
|
|
14597
15885
|
}
|
|
15886
|
+
#viewState;
|
|
14598
15887
|
#modelUnsubscribe;
|
|
14599
15888
|
#pendingKeys;
|
|
14600
15889
|
#aborters;
|
|
@@ -14690,40 +15979,38 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14690
15979
|
this.#unregisterSearchCommands?.();
|
|
14691
15980
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
14692
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
|
+
}
|
|
14693
15986
|
updated(changedProps) {
|
|
14694
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
14695
15987
|
if (changedProps.has(`model`)) {
|
|
14696
15988
|
this.#modelUnsubscribe?.();
|
|
14697
15989
|
this.#modelUnsubscribe = void 0;
|
|
14698
|
-
if (this.model) {
|
|
14699
|
-
|
|
14700
|
-
|
|
14701
|
-
|
|
14702
|
-
|
|
14703
|
-
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
14704
|
-
});
|
|
14705
|
-
this.#resetViewState();
|
|
14706
|
-
}
|
|
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
|
+
});
|
|
14707
15995
|
}
|
|
14708
|
-
if (changedProps.has(`interactionMode`)) {
|
|
15996
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
|
|
14709
15997
|
this.#anchorIndex = void 0;
|
|
14710
15998
|
this.clearSelection();
|
|
14711
15999
|
}
|
|
14712
16000
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
14713
|
-
if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
14714
16001
|
this.#ensureVisibleChildren();
|
|
16002
|
+
this.#viewState.continueRestoreLoads();
|
|
14715
16003
|
if (this.#pendingFlip) {
|
|
14716
16004
|
const flip = this.#pendingFlip;
|
|
14717
16005
|
this.#pendingFlip = void 0;
|
|
14718
16006
|
this.#applyFlip(flip);
|
|
14719
16007
|
}
|
|
14720
16008
|
}
|
|
14721
|
-
|
|
16009
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
16010
|
+
#onDataReset() {
|
|
14722
16011
|
this.#pendingFlip = void 0;
|
|
14723
|
-
this.#clearSelectionInternal();
|
|
14724
|
-
this.tickled.clearTickled();
|
|
14725
|
-
this._zoomKey = void 0;
|
|
14726
16012
|
this.#anchorIndex = void 0;
|
|
16013
|
+
this.#viewState.invalidate();
|
|
14727
16014
|
}
|
|
14728
16015
|
#ensureVisibleChildren() {
|
|
14729
16016
|
if (!this.model && !this.loadChildren) return;
|
|
@@ -14754,7 +16041,10 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14754
16041
|
} finally {
|
|
14755
16042
|
this.#pendingKeys.delete(key);
|
|
14756
16043
|
this.#aborters.delete(key);
|
|
14757
|
-
if (!controller.signal.aborted)
|
|
16044
|
+
if (!controller.signal.aborted) {
|
|
16045
|
+
this.#viewState.invalidate();
|
|
16046
|
+
this.requestUpdate();
|
|
16047
|
+
}
|
|
14758
16048
|
}
|
|
14759
16049
|
}
|
|
14760
16050
|
#canSelect(node) {
|
|
@@ -14768,6 +16058,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14768
16058
|
this.selectedNode = void 0;
|
|
14769
16059
|
}
|
|
14770
16060
|
#dispatchSelect(previous) {
|
|
16061
|
+
this.#viewState.selectionChanged();
|
|
14771
16062
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
14772
16063
|
detail: {
|
|
14773
16064
|
selected: new Set(this._selectedNodes),
|
|
@@ -14795,6 +16086,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14795
16086
|
this.requestUpdate();
|
|
14796
16087
|
}
|
|
14797
16088
|
clearSelection() {
|
|
16089
|
+
this.#viewState.clearPendingSelection();
|
|
14798
16090
|
if (this._selectedNodes.size === 0) return;
|
|
14799
16091
|
const prev = new Set(this._selectedNodes);
|
|
14800
16092
|
this.#clearSelectionInternal();
|
|
@@ -14833,7 +16125,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14833
16125
|
zoomTo(node) {
|
|
14834
16126
|
if (this._zoomKey === node.item.key) return;
|
|
14835
16127
|
this.#pendingFlip = this.#planFlip(node.item.key);
|
|
14836
|
-
this
|
|
16128
|
+
this.#setZoom(node.item.key);
|
|
14837
16129
|
this.tickled.clearTickled();
|
|
14838
16130
|
const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
|
|
14839
16131
|
const absoluteDepth = depth === void 0 ? -1 : depth - 2;
|
|
@@ -14865,7 +16157,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14865
16157
|
const previous = this.focusedNode;
|
|
14866
16158
|
const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
|
|
14867
16159
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
14868
|
-
this
|
|
16160
|
+
this.#setZoom(void 0);
|
|
14869
16161
|
this.tickled.clearTickled();
|
|
14870
16162
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
14871
16163
|
detail: {
|
|
@@ -14885,6 +16177,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14885
16177
|
}));
|
|
14886
16178
|
this.requestUpdate();
|
|
14887
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
|
+
}
|
|
14888
16199
|
/**
|
|
14889
16200
|
* Work out (before `_zoomKey` changes) whether the upcoming zoom should
|
|
14890
16201
|
* animate, and how — see `FlipPlan`. Returns `undefined` to skip the
|
|
@@ -14994,10 +16305,20 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14994
16305
|
const ancestor = this.#resolveKeyPath(path.slice(0, path.length - this.maxDepth));
|
|
14995
16306
|
if (ancestor) this.zoomTo(ancestor);
|
|
14996
16307
|
}
|
|
14997
|
-
this
|
|
16308
|
+
this.#setKeyboardCursor(key);
|
|
14998
16309
|
this.requestUpdate();
|
|
14999
16310
|
return true;
|
|
15000
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
|
+
}
|
|
15001
16322
|
#resolveKeyPath(keys) {
|
|
15002
16323
|
let current = this.#effectiveRoot;
|
|
15003
16324
|
for (const key of keys) {
|
|
@@ -15027,6 +16348,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15027
16348
|
depth: r.depth
|
|
15028
16349
|
}));
|
|
15029
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
|
+
}
|
|
15030
16370
|
#matchesFilter(node, depth) {
|
|
15031
16371
|
if (!this.filterPredicate) return true;
|
|
15032
16372
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -15128,7 +16468,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15128
16468
|
const moveTo = (rect) => {
|
|
15129
16469
|
if (!rect) return;
|
|
15130
16470
|
event.preventDefault();
|
|
15131
|
-
this
|
|
16471
|
+
this.#setKeyboardCursor(rect.node.item.key);
|
|
15132
16472
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
15133
16473
|
};
|
|
15134
16474
|
switch (event.key) {
|
|
@@ -15309,7 +16649,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15309
16649
|
e.stopPropagation();
|
|
15310
16650
|
this.#handleItemDblClick(rect);
|
|
15311
16651
|
}}
|
|
15312
|
-
@pointerenter=${() => this
|
|
16652
|
+
@pointerenter=${() => this.#onTilePointerEnter(node.item.key)}
|
|
15313
16653
|
>
|
|
15314
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>`}
|
|
15315
16655
|
${childrenContent}
|
|
@@ -15623,6 +16963,7 @@ __decorate([property({
|
|
|
15623
16963
|
reflect: true
|
|
15624
16964
|
})], TreemapListElement.prototype, "searchModes", void 0);
|
|
15625
16965
|
__decorate([state()], TreemapListElement.prototype, "_zoomKey", void 0);
|
|
16966
|
+
__decorate([property({ attribute: false })], TreemapListElement.prototype, "restoreLoads", void 0);
|
|
15626
16967
|
TreemapListElement = __decorate([safeCustomElement(`ixfx-treemap-list`)], TreemapListElement);
|
|
15627
16968
|
//#endregion
|
|
15628
16969
|
//#region src/twosplit-layout/split-layout.ts
|
|
@@ -15649,6 +16990,10 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15649
16990
|
this.#nestedSplitInitialSizeA = ``;
|
|
15650
16991
|
this.#pendingNestedUpdate = null;
|
|
15651
16992
|
this.#hysteresisThreshold = .1;
|
|
16993
|
+
this.#viewState = new ViewStateController(this, {
|
|
16994
|
+
kind: `twosplit-layout`,
|
|
16995
|
+
capture: () => this.getViewState()
|
|
16996
|
+
});
|
|
15652
16997
|
this.#onResizeHandleDown = (event) => {
|
|
15653
16998
|
event.preventDefault();
|
|
15654
16999
|
const zoneEl = this.getZone();
|
|
@@ -15665,6 +17010,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15665
17010
|
if (zoneEl) zoneEl.classList.remove(`user-resizing`);
|
|
15666
17011
|
window.removeEventListener(`pointerup`, this.#onResizeHandleUp);
|
|
15667
17012
|
window.removeEventListener(`pointermove`, this.#onResizeHandleMove);
|
|
17013
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15668
17014
|
};
|
|
15669
17015
|
this.#onResizeHandleMove = (event) => {
|
|
15670
17016
|
const bounds = this.#elementBounds;
|
|
@@ -15784,9 +17130,11 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15784
17130
|
if (nestedSplit) {
|
|
15785
17131
|
const nestedZoneEl = nestedSplit.getZone();
|
|
15786
17132
|
if (nestedZoneEl) nestedZoneEl.classList.remove(`user-resizing`);
|
|
17133
|
+
if (#viewState in nestedSplit && nestedSplit.sizeA !== this.#nestedSplitInitialSizeA) nestedSplit.#viewState.notify();
|
|
15787
17134
|
}
|
|
15788
17135
|
window.removeEventListener(`pointerup`, this.#onCornerResizeUp);
|
|
15789
17136
|
window.removeEventListener(`pointermove`, this.#onCornerResizeMove);
|
|
17137
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15790
17138
|
};
|
|
15791
17139
|
this.#onCornerResizeMove = (event) => {
|
|
15792
17140
|
const outerBounds = this.#elementBounds;
|
|
@@ -15854,6 +17202,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15854
17202
|
#collapsedAPointerAtCollapse;
|
|
15855
17203
|
#collapsedBPointerAtCollapse;
|
|
15856
17204
|
#hysteresisThreshold;
|
|
17205
|
+
#viewState;
|
|
15857
17206
|
disconnectedCallback() {
|
|
15858
17207
|
super.disconnectedCallback();
|
|
15859
17208
|
this.#observer?.disconnect();
|
|
@@ -15863,7 +17212,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15863
17212
|
}
|
|
15864
17213
|
updated(changedProperties) {
|
|
15865
17214
|
if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateCornerPosition();
|
|
15866
|
-
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();
|
|
15867
17216
|
}
|
|
15868
17217
|
#updateDataCollapsed() {
|
|
15869
17218
|
if (this.collapsedA) this.setAttribute(`data-collapsed-a`, ``);
|
|
@@ -16022,6 +17371,31 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
16022
17371
|
else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) cornerZone.style.cursor = `nesw-resize`;
|
|
16023
17372
|
}
|
|
16024
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() {}
|
|
16025
17399
|
getZone() {
|
|
16026
17400
|
if (!this.shadowRoot) return;
|
|
16027
17401
|
const el = this.shadowRoot.querySelector(`#zone`);
|
|
@@ -16209,6 +17583,12 @@ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSi
|
|
|
16209
17583
|
__decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeB", void 0);
|
|
16210
17584
|
__decorate([state()], TwoSplitLayoutElement.prototype, "_hasCorner", void 0);
|
|
16211
17585
|
TwoSplitLayoutElement = __decorate([safeCustomElement(`ixfx-twosplit-layout`)], TwoSplitLayoutElement);
|
|
17586
|
+
function sideState(size, collapsed) {
|
|
17587
|
+
return collapsed ? {
|
|
17588
|
+
size,
|
|
17589
|
+
collapsed
|
|
17590
|
+
} : { size };
|
|
17591
|
+
}
|
|
16212
17592
|
//#endregion
|
|
16213
17593
|
//#region src/util/checkbox.ts
|
|
16214
17594
|
function isCheckboxLike(el) {
|
|
@@ -16267,4 +17647,4 @@ function init() {
|
|
|
16267
17647
|
console.log(`Init`);
|
|
16268
17648
|
}
|
|
16269
17649
|
//#endregion
|
|
16270
|
-
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, 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, 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, computeSunburstLayout, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createValueResolver, createVerticalListAdapter, decomposeViewIsometry, defaultGroupLabel, defaultSunburstValueAccessor, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findEmptiedStacks, findHyperbolicIdOf, findHyperbolicKeyPath, findHyperbolicNode, findHyperbolicPath, findHyperbolicPathById, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findSunburstId, findSunburstNodeById, 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, isSunburstAncestorId, 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, sunburstChildId, sunburstHasChildren, sunburstIdToPath, sunburstParentId, sunburstPathToId, 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 };
|