@ixfx/components 0.15.5 → 0.16.1
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 +986 -49
- package/bundle/index.js +765 -729
- package/dist/ac-token.js +1 -1
- package/dist/crumb-navigation-BbqfmkYs.d.ts +147 -0
- package/dist/crumb-navigation-YnFVFqrx.js +1290 -0
- package/dist/crumbs.d.ts +2 -115
- package/dist/crumbs.js +1 -1179
- package/dist/{data-grid-C1sZX6n9.js → data-grid-Cqxuwgbb.js} +615 -210
- package/dist/{data-grid-D7qzmMMp.d.ts → data-grid-Csh5EJW9.d.ts} +55 -4
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +1 -1
- package/dist/{detail-list-q8bs67BM.js → detail-list-Bo5ze3SN.js} +234 -9
- package/dist/{detail-list-CKIKlszv.d.ts → detail-list-DSZ1DDor.d.ts} +48 -4
- package/dist/detail-list.d.ts +4 -4
- package/dist/detail-list.js +1 -1
- package/dist/{flat-list-DXMyDiSH.d.ts → flat-list-CT9kX3TQ.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-D0dJNVEF.js} +278 -76
- package/dist/{grid-list-0e7snW6t.d.ts → grid-list-DDLrWRxm.d.ts} +128 -22
- package/dist/grid-list.d.ts +3 -3
- package/dist/grid-list.js +1 -1
- package/dist/{incr-search-JIhwB49y.js → incr-search-CgbL-OlS.js} +140 -10
- package/dist/incr-search.d.ts +1 -1
- package/dist/incr-search.js +1 -1
- package/dist/index-9ttcdwlL.d.ts +71 -0
- package/dist/index-Bnm-kKGC.d.ts +475 -0
- package/dist/{index-KN2MW_xD.d.ts → index-CUErGhCg.d.ts} +31 -4
- package/dist/{index-DEVyVIn8.d.ts → index-D-lZNlHa.d.ts} +52 -6
- package/dist/index.d.ts +341 -21
- package/dist/index.js +2286 -884
- 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-DUqiUAWQ.js +2230 -0
- package/dist/miller.d.ts +4 -437
- package/dist/miller.js +1 -1815
- package/dist/narrowed-text.js +1 -1
- 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_M0YTEA.js} +299 -80
- package/dist/{sunbursttree-list--Z0uyh8O.d.ts → sunbursttree-list-DePuFYlU.d.ts} +40 -3
- package/dist/tree-CftHwtEb.js +146 -0
- package/dist/{tree-D8VxvR9u.js → tree-DAlgvaHB.js} +276 -48
- 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-DyKPKGxK.js +1993 -0
- package/dist/{vertical-list-D8MksoTp.d.ts → vertical-list-SAx56U-l.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 +79 -5
- package/docs-user/detail-list.md +69 -0
- package/docs-user/dock.md +63 -12
- package/docs-user/flat-list.md +27 -0
- package/docs-user/grid-list.md +69 -13
- 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 +2 -2
package/dist/index.js
CHANGED
|
@@ -8,7 +8,7 @@ import { a as iconBus, c as setIconFont, i as hasIcon, l as svgToDataUri, n as g
|
|
|
8
8
|
import { t as IxfxIconElement } from "./icon-1gpwtPx8.js";
|
|
9
9
|
import { AcTextElement } from "./ac-text.js";
|
|
10
10
|
import { t as DataController } from "./data-controller-DOlmqNKb.js";
|
|
11
|
-
import { a as LIST_SEARCH_COMMAND_IDS, c as createElementListSearchAdapter, d as createDataListSearchAdapter, f as IncrSearchTreeController, h as createFuzzySearch, i as ListSearchController, l as KeyedElementIndex, m as createAcCompletionProvider, n as listSearchStyles, o as registerListSearchCommands, p as createIncrSearch, r as LIST_SEARCH_MODES, s as createElementIncrSearch, t as createTreeListSearchAdapter, u as keyForElement } from "./incr-search-
|
|
11
|
+
import { a as LIST_SEARCH_COMMAND_IDS, c as createElementListSearchAdapter, d as createDataListSearchAdapter, f as IncrSearchTreeController, h as createFuzzySearch, i as ListSearchController, l as KeyedElementIndex, m as createAcCompletionProvider, n as listSearchStyles, o as registerListSearchCommands, p as createIncrSearch, r as LIST_SEARCH_MODES, s as createElementIncrSearch, t as createTreeListSearchAdapter, u as keyForElement } from "./incr-search-CgbL-OlS.js";
|
|
12
12
|
import { AcTokenElement } from "./ac-token.js";
|
|
13
13
|
import { t as handleStyles } from "./handle-DAXs7kbJ.js";
|
|
14
14
|
import { TitlebarElement } from "./titlebar.js";
|
|
@@ -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-DAlgvaHB.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-Cqxuwgbb.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-Bo5ze3SN.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-DyKPKGxK.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-D0dJNVEF.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-DUqiUAWQ.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_M0YTEA.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,23 +3559,71 @@ 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
|
-
|
|
3521
|
-
|
|
3522
|
-
|
|
3604
|
+
let resizeObserver;
|
|
3605
|
+
function sharedResizeObserver() {
|
|
3606
|
+
resizeObserver ??= new ResizeObserver((entries) => {
|
|
3607
|
+
for (const entry of entries) resizeHandlers.get(entry.target)?.(entry.contentRect.width);
|
|
3608
|
+
});
|
|
3609
|
+
return resizeObserver;
|
|
3610
|
+
}
|
|
3523
3611
|
const mutationHandlers = /* @__PURE__ */ new Map();
|
|
3524
|
-
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
const
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3612
|
+
let mutationObserver;
|
|
3613
|
+
function sharedMutationObserver() {
|
|
3614
|
+
mutationObserver ??= new MutationObserver((records) => {
|
|
3615
|
+
const touched = /* @__PURE__ */ new Set();
|
|
3616
|
+
for (const record of records) for (let node = record.target; node; node = node.parentNode) {
|
|
3617
|
+
const handler = mutationHandlers.get(node);
|
|
3618
|
+
if (handler) {
|
|
3619
|
+
touched.add(handler);
|
|
3620
|
+
break;
|
|
3621
|
+
}
|
|
3531
3622
|
}
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3623
|
+
for (const handler of touched) handler();
|
|
3624
|
+
});
|
|
3625
|
+
return mutationObserver;
|
|
3626
|
+
}
|
|
3535
3627
|
let TabListItemElement = class TabListItemElement extends LitElement {
|
|
3536
3628
|
constructor(..._args) {
|
|
3537
3629
|
super(..._args);
|
|
@@ -3565,10 +3657,10 @@ let TabListItemElement = class TabListItemElement extends LitElement {
|
|
|
3565
3657
|
resizeHandlers.set(this, (width) => {
|
|
3566
3658
|
this._compact = width < 64;
|
|
3567
3659
|
});
|
|
3568
|
-
sharedResizeObserver.observe(this);
|
|
3660
|
+
sharedResizeObserver().observe(this);
|
|
3569
3661
|
this._wireSlotListeners();
|
|
3570
3662
|
mutationHandlers.set(this, this._onLabelMutation);
|
|
3571
|
-
sharedMutationObserver.observe(this, {
|
|
3663
|
+
sharedMutationObserver().observe(this, {
|
|
3572
3664
|
childList: true,
|
|
3573
3665
|
characterData: true,
|
|
3574
3666
|
subtree: true,
|
|
@@ -3578,7 +3670,7 @@ let TabListItemElement = class TabListItemElement extends LitElement {
|
|
|
3578
3670
|
}
|
|
3579
3671
|
disconnectedCallback() {
|
|
3580
3672
|
super.disconnectedCallback();
|
|
3581
|
-
sharedResizeObserver.unobserve(this);
|
|
3673
|
+
sharedResizeObserver().unobserve(this);
|
|
3582
3674
|
resizeHandlers.delete(this);
|
|
3583
3675
|
this._commandCleanup?.();
|
|
3584
3676
|
this._commandCleanup = void 0;
|
|
@@ -4159,6 +4251,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4159
4251
|
this._toolbarSlots = /* @__PURE__ */ new Set();
|
|
4160
4252
|
this._toolbarObserver = null;
|
|
4161
4253
|
this._panelObservers = /* @__PURE__ */ new Map();
|
|
4254
|
+
this.#viewState = new ViewStateController(this, {
|
|
4255
|
+
kind: `dock-host`,
|
|
4256
|
+
capture: () => this.getViewState()
|
|
4257
|
+
});
|
|
4258
|
+
this.#layoutRestored = false;
|
|
4162
4259
|
this._onDragStart = (event) => {
|
|
4163
4260
|
if (event.composedPath().find((n) => n instanceof HTMLElement && n.tagName === `IXFX-DOCK-HOST`) !== this) return;
|
|
4164
4261
|
const { panelId, pointerId, clientX, clientY } = event.detail;
|
|
@@ -4191,6 +4288,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4191
4288
|
this._onSplitWeightsChanged = () => {
|
|
4192
4289
|
this._foldLiveWeights();
|
|
4193
4290
|
this._scheduleJunctions();
|
|
4291
|
+
this.#viewState.notify();
|
|
4292
|
+
};
|
|
4293
|
+
this._onInnerViewState = (event) => {
|
|
4294
|
+
if (event.target === event.currentTarget) event.stopPropagation();
|
|
4194
4295
|
};
|
|
4195
4296
|
this._onSplitCollapsed = (event, collapsed) => {
|
|
4196
4297
|
const detail = event.detail;
|
|
@@ -4201,6 +4302,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4201
4302
|
if (node === null || !isStack(node)) return;
|
|
4202
4303
|
if (collapsed) node.collapsed = true;
|
|
4203
4304
|
else delete node.collapsed;
|
|
4305
|
+
this.#viewState.notify();
|
|
4204
4306
|
};
|
|
4205
4307
|
this._onSplitClose = (event) => {
|
|
4206
4308
|
event.preventDefault();
|
|
@@ -4223,6 +4325,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4223
4325
|
if (stack === null || !isStack(stack)) return;
|
|
4224
4326
|
if (!setActive(stack, panelId)) return;
|
|
4225
4327
|
this.requestUpdate();
|
|
4328
|
+
this.#viewState.notify();
|
|
4226
4329
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4227
4330
|
detail: { panelId },
|
|
4228
4331
|
bubbles: true,
|
|
@@ -4273,6 +4376,9 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4273
4376
|
this._teardownTabDragListeners();
|
|
4274
4377
|
};
|
|
4275
4378
|
}
|
|
4379
|
+
#viewState;
|
|
4380
|
+
/** A layout was restored via `setViewState()`; it wins over the initial `layout` attribute. */
|
|
4381
|
+
#layoutRestored;
|
|
4276
4382
|
connectedCallback() {
|
|
4277
4383
|
super.connectedCallback();
|
|
4278
4384
|
this._buildInitialModel();
|
|
@@ -4310,7 +4416,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4310
4416
|
}
|
|
4311
4417
|
willUpdate(changed) {
|
|
4312
4418
|
super.willUpdate(changed);
|
|
4313
|
-
|
|
4419
|
+
const initial = !this.hasUpdated && this.#layoutRestored;
|
|
4420
|
+
if (changed.has(`layout`) && this.layout !== null && !initial) {
|
|
4314
4421
|
const parsed = parseLayout(this.layout);
|
|
4315
4422
|
if (parsed) {
|
|
4316
4423
|
this._model = parsed.root;
|
|
@@ -4342,6 +4449,10 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4342
4449
|
this._scheduleJunctions();
|
|
4343
4450
|
}
|
|
4344
4451
|
_buildInitialModel() {
|
|
4452
|
+
if (this.#layoutRestored) {
|
|
4453
|
+
this._mergePoolIntoModel();
|
|
4454
|
+
return;
|
|
4455
|
+
}
|
|
4345
4456
|
if (this.layout !== null) {
|
|
4346
4457
|
const parsed = parseLayout(this.layout);
|
|
4347
4458
|
if (parsed) {
|
|
@@ -4476,21 +4587,69 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4476
4587
|
}
|
|
4477
4588
|
this._pool = current;
|
|
4478
4589
|
this.requestUpdate();
|
|
4479
|
-
if (structuralChange) this._emitChange();
|
|
4590
|
+
if (structuralChange) this._emitChange(`data`);
|
|
4591
|
+
}
|
|
4592
|
+
/**
|
|
4593
|
+
* Snapshot the layout (live split weights folded in from the DOM). With
|
|
4594
|
+
* `includePending` (default), panels the restored layout names but the
|
|
4595
|
+
* host doesn't hold yet are kept in place; without it they are left out.
|
|
4596
|
+
*/
|
|
4597
|
+
getViewState(options) {
|
|
4598
|
+
const layout = this._captureLayout();
|
|
4599
|
+
const withPending = options?.includePending ?? true;
|
|
4600
|
+
return this.#viewState.shape({ layout: withPending ? layout : withoutAbsentPanels(layout, (id) => this._findPanelEl(id) !== void 0) }, options);
|
|
4601
|
+
}
|
|
4602
|
+
/**
|
|
4603
|
+
* Restore a layout. Panels it names that aren't in the host yet keep their
|
|
4604
|
+
* place (shown as a placeholder tab) and slot in when they arrive — as a
|
|
4605
|
+
* light-DOM child, or via `addPanel()` without a target. Pooled panels it
|
|
4606
|
+
* doesn't name are appended to the first stack admitting their kind.
|
|
4607
|
+
*/
|
|
4608
|
+
setViewState(state) {
|
|
4609
|
+
if (!this.#viewState.accepts(state) || state.layout === void 0) return;
|
|
4610
|
+
if (!isDockLayout(state.layout)) {
|
|
4611
|
+
console.warn(`[dock-host] ignoring view state with a malformed layout`);
|
|
4612
|
+
return;
|
|
4613
|
+
}
|
|
4614
|
+
const layout = state.layout;
|
|
4615
|
+
this.#viewState.restoring(() => {
|
|
4616
|
+
this.#layoutRestored = true;
|
|
4617
|
+
this._returnAllToolbars();
|
|
4618
|
+
this._loadGeneration++;
|
|
4619
|
+
this._model = normalize(cloneNode(layout.root));
|
|
4620
|
+
this._mergePoolIntoModel();
|
|
4621
|
+
this._liveStacks = snapshotNonEmptyStacks(this._model);
|
|
4622
|
+
this.requestUpdate();
|
|
4623
|
+
this._emitChange();
|
|
4624
|
+
});
|
|
4625
|
+
}
|
|
4626
|
+
/**
|
|
4627
|
+
* Drop every placeholder: panels the restored layout names that the host
|
|
4628
|
+
* doesn't hold. Call it once the app has added all the panels it has, so
|
|
4629
|
+
* ids for panels that no longer exist don't linger (and get saved again).
|
|
4630
|
+
*/
|
|
4631
|
+
clearPendingViewState() {
|
|
4632
|
+
const placeholders = collectPanelIds(this._model).filter((id) => this._isPlaceholder(id));
|
|
4633
|
+
if (placeholders.length === 0) return;
|
|
4634
|
+
for (const id of placeholders) this._removeFromModel(id);
|
|
4635
|
+
this._commit();
|
|
4480
4636
|
}
|
|
4481
|
-
/**
|
|
4637
|
+
/** @deprecated Since 0.16; use `getViewState().layout`. */
|
|
4482
4638
|
saveLayout() {
|
|
4639
|
+
return this._captureLayout();
|
|
4640
|
+
}
|
|
4641
|
+
/** @deprecated Since 0.16; use `setViewState({ layout })`. */
|
|
4642
|
+
loadLayout(layout) {
|
|
4643
|
+
this.setViewState({ layout });
|
|
4644
|
+
}
|
|
4645
|
+
/** The current layout, folding live split weights from the DOM. */
|
|
4646
|
+
_captureLayout() {
|
|
4483
4647
|
this._foldLiveWeights();
|
|
4484
4648
|
return serializeLayout(this._model);
|
|
4485
4649
|
}
|
|
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();
|
|
4650
|
+
/** Whether `panelId` is in the model only as a placeholder from a restored layout. */
|
|
4651
|
+
_isPlaceholder(panelId) {
|
|
4652
|
+
return !this._pool.some((p) => p.panelId === panelId) && collectPanelIds(this._model).includes(panelId);
|
|
4494
4653
|
}
|
|
4495
4654
|
/**
|
|
4496
4655
|
* Dock a panel. Returns false (without touching the DOM) when the
|
|
@@ -4537,11 +4696,12 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4537
4696
|
if (panel.collapsible !== void 0) el.collapsible = panel.collapsible;
|
|
4538
4697
|
if (panel.kind !== void 0) el.kind = panel.kind;
|
|
4539
4698
|
}
|
|
4699
|
+
const takesSavedPlace = target === void 0 && this._isPlaceholder(el.panelId);
|
|
4540
4700
|
if (el.parentElement !== this) this.appendChild(el);
|
|
4541
4701
|
if (!this._pool.some((p) => p.panelId === el.panelId)) this._pool = [...this._pool, el];
|
|
4542
4702
|
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`);
|
|
4703
|
+
if (!takesSavedPlace && collectPanelIds(this._model).includes(el.panelId)) this._removeFromModel(el.panelId);
|
|
4704
|
+
if (takesSavedPlace) {} else if (splitTarget !== null) insertBeside(this._model, splitTarget, opToDirection(op), createStack([el.panelId]), `after`);
|
|
4545
4705
|
else if (stackPath !== null) {
|
|
4546
4706
|
const stack = findNode(this._model, stackPath);
|
|
4547
4707
|
if (stack !== null && isStack(stack)) addPanelToStack(stack, el.panelId, target?.index);
|
|
@@ -4582,6 +4742,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4582
4742
|
if (stack === null || !isStack(stack)) return false;
|
|
4583
4743
|
if (!setActive(stack, panelId)) return false;
|
|
4584
4744
|
this.requestUpdate();
|
|
4745
|
+
this.#viewState.notify();
|
|
4585
4746
|
this.dispatchEvent(new CustomEvent(`dock-panel-activated`, {
|
|
4586
4747
|
detail: { panelId },
|
|
4587
4748
|
bubbles: true,
|
|
@@ -4641,7 +4802,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4641
4802
|
* {@link setLeafPlacementPolicy}'s `allowEdgeSplit` (default `true`).
|
|
4642
4803
|
* Pass `[]` to seal the leaf (nothing admitted). Warns when no stack
|
|
4643
4804
|
* currently carries `name`, but the rule is still stored and applies once
|
|
4644
|
-
* one appears. Rules are host policy: kept across `
|
|
4805
|
+
* one appears. Rules are host policy: kept across `setViewState()`, never
|
|
4645
4806
|
* serialized.
|
|
4646
4807
|
*/
|
|
4647
4808
|
setLeafExclusiveContents(name, kinds) {
|
|
@@ -4688,7 +4849,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4688
4849
|
* unconstrained* leaf — the policy is not propagated to it.
|
|
4689
4850
|
*
|
|
4690
4851
|
* Warns when no stack currently carries `name`; the policy is stored and
|
|
4691
|
-
* applies once one appears. Host policy: kept across `
|
|
4852
|
+
* applies once one appears. Host policy: kept across `setViewState()`, never
|
|
4692
4853
|
* serialized.
|
|
4693
4854
|
*/
|
|
4694
4855
|
setLeafPlacementPolicy(name, policy) {
|
|
@@ -4837,7 +4998,8 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4837
4998
|
* already docked here or its kind is refused by `acceptsKinds`.
|
|
4838
4999
|
*/
|
|
4839
5000
|
acceptExternalPanel(snapshot, at) {
|
|
4840
|
-
|
|
5001
|
+
const placeholder = this._isPlaceholder(snapshot.panelId);
|
|
5002
|
+
if (!placeholder && collectPanelIds(this._model).includes(snapshot.panelId)) return null;
|
|
4841
5003
|
if (this.acceptsKinds !== null && !this.acceptsKinds.includes(normalizeKind(snapshot.kind))) return null;
|
|
4842
5004
|
const el = document.createElement(`ixfx-dock-panel`);
|
|
4843
5005
|
el.panelId = snapshot.panelId;
|
|
@@ -4850,7 +5012,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
4850
5012
|
if (`contentHTML` in snapshot && snapshot.contentHTML) el.innerHTML = snapshot.contentHTML;
|
|
4851
5013
|
this._returnAllToolbars();
|
|
4852
5014
|
this._pool = [...this._pool, el];
|
|
4853
|
-
this._appendPanelToFirstStack(el.panelId);
|
|
5015
|
+
if (!placeholder) this._appendPanelToFirstStack(el.panelId);
|
|
4854
5016
|
this.appendChild(el);
|
|
4855
5017
|
normalize(this._model);
|
|
4856
5018
|
this.endExternalDrop();
|
|
@@ -5346,10 +5508,11 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5346
5508
|
hostEmpty: now.size === 0
|
|
5347
5509
|
});
|
|
5348
5510
|
}
|
|
5349
|
-
_emitChange() {
|
|
5350
|
-
const layout = this.
|
|
5511
|
+
_emitChange(reason = `user`) {
|
|
5512
|
+
const layout = this._captureLayout();
|
|
5351
5513
|
this._announceEmptiedStacks();
|
|
5352
5514
|
this._emit(`dock-change`, { layout });
|
|
5515
|
+
this.#viewState.notify(reason);
|
|
5353
5516
|
}
|
|
5354
5517
|
render() {
|
|
5355
5518
|
return html`
|
|
@@ -5385,6 +5548,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5385
5548
|
return html`
|
|
5386
5549
|
<ixfx-split-layout
|
|
5387
5550
|
class="split"
|
|
5551
|
+
forget-absent
|
|
5388
5552
|
direction=${node.direction}
|
|
5389
5553
|
size=${size === void 0 ? nothing : live(String(size))}
|
|
5390
5554
|
style=${`--split-gutter-size: var(--dock-split-bar-size, 2px); --split-collapsed-size: var(--dock-collapsed-size, 40px)`}
|
|
@@ -5395,6 +5559,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5395
5559
|
@ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
|
|
5396
5560
|
@ixfx-split-expand=${(e) => this._onSplitCollapsed(e, false)}
|
|
5397
5561
|
@ixfx-split-close=${this._onSplitClose}
|
|
5562
|
+
@view-state-change=${this._onInnerViewState}
|
|
5398
5563
|
>
|
|
5399
5564
|
${node.children.map((child, i) => this._renderNode(child, [...path, i], node.sizes[i]))}
|
|
5400
5565
|
</ixfx-split-layout>
|
|
@@ -5483,7 +5648,7 @@ let DockHostElement = class DockHostElement extends LitElement {
|
|
|
5483
5648
|
.iconName=${el?.iconName ?? ``}
|
|
5484
5649
|
.closeable=${el?.closable ?? false}
|
|
5485
5650
|
@close=${() => this.removePanel(panelId)}
|
|
5486
|
-
>${el?.title ?? panelId}</ixfx-tab-list-item>
|
|
5651
|
+
>${el?.title ?? this.placeholderLabel?.(panelId) ?? panelId}</ixfx-tab-list-item>
|
|
5487
5652
|
`;
|
|
5488
5653
|
}
|
|
5489
5654
|
_stripCaretRect(strip, gapIndex, hostRect) {
|
|
@@ -5702,6 +5867,7 @@ __decorate([property({
|
|
|
5702
5867
|
converter: { fromAttribute: (value) => value === null ? null : value.split(/\s+/).filter((k) => k.length > 0) }
|
|
5703
5868
|
})], DockHostElement.prototype, "acceptsKinds", void 0);
|
|
5704
5869
|
__decorate([property({ attribute: false })], DockHostElement.prototype, "canAccept", void 0);
|
|
5870
|
+
__decorate([property({ attribute: false })], DockHostElement.prototype, "placeholderLabel", void 0);
|
|
5705
5871
|
__decorate([state()], DockHostElement.prototype, "_model", void 0);
|
|
5706
5872
|
__decorate([state()], DockHostElement.prototype, "_pool", void 0);
|
|
5707
5873
|
__decorate([state()], DockHostElement.prototype, "_dropZone", void 0);
|
|
@@ -6768,269 +6934,6 @@ function createGestureHandlers(host) {
|
|
|
6768
6934
|
};
|
|
6769
6935
|
}
|
|
6770
6936
|
//#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
6937
|
//#region src/hyperbolic-tree/layout.ts
|
|
7035
6938
|
const ID_SEPARATOR = `\u001F`;
|
|
7036
6939
|
/**
|
|
@@ -7445,6 +7348,564 @@ function computeHyperbolicLayout(root, options = {}) {
|
|
|
7445
7348
|
};
|
|
7446
7349
|
}
|
|
7447
7350
|
//#endregion
|
|
7351
|
+
//#region src/hyperbolic-tree/hyperbolic-view-state.ts
|
|
7352
|
+
function indexHyperbolicKeys(root) {
|
|
7353
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
7354
|
+
const nodes = /* @__PURE__ */ new Set();
|
|
7355
|
+
const walk = (node) => {
|
|
7356
|
+
nodes.add(node);
|
|
7357
|
+
if (!byKey.has(node.item.key)) byKey.set(node.item.key, node);
|
|
7358
|
+
for (const child of node.children ?? []) walk(child);
|
|
7359
|
+
};
|
|
7360
|
+
if (root) walk(root);
|
|
7361
|
+
return {
|
|
7362
|
+
byKey,
|
|
7363
|
+
nodes
|
|
7364
|
+
};
|
|
7365
|
+
}
|
|
7366
|
+
/** Key path from the root's children to the node with layout id `id`. */
|
|
7367
|
+
function centerPathOf(root, id) {
|
|
7368
|
+
if (id === void 0) return [];
|
|
7369
|
+
return findHyperbolicPathById(root, id)?.slice(1).map((n) => n.item.key) ?? [];
|
|
7370
|
+
}
|
|
7371
|
+
/** Walk `path` from the root, matching each step by key among the loaded children. */
|
|
7372
|
+
function resolveCenterPath(root, path) {
|
|
7373
|
+
if (!root) return { kind: `missing` };
|
|
7374
|
+
let node = root;
|
|
7375
|
+
let id = root.item.key;
|
|
7376
|
+
for (const [depth, key] of path.entries()) {
|
|
7377
|
+
const children = node.children;
|
|
7378
|
+
if (children === void 0) return node.item.isLeaf === true ? { kind: `missing` } : {
|
|
7379
|
+
kind: `load`,
|
|
7380
|
+
node,
|
|
7381
|
+
depth
|
|
7382
|
+
};
|
|
7383
|
+
const index = children.findIndex((c) => c.item.key === key);
|
|
7384
|
+
if (index < 0) return { kind: `missing` };
|
|
7385
|
+
id = hyperbolicChildIds(id, children)[index];
|
|
7386
|
+
node = children[index];
|
|
7387
|
+
}
|
|
7388
|
+
return {
|
|
7389
|
+
kind: `found`,
|
|
7390
|
+
id: path.length === 0 ? void 0 : id
|
|
7391
|
+
};
|
|
7392
|
+
}
|
|
7393
|
+
/**
|
|
7394
|
+
* As `reconcileSelection` in `tree/tree-view-state.ts`, but a selected node
|
|
7395
|
+
* still in the tree is kept by identity — keys may repeat here, so re-keying
|
|
7396
|
+
* it would jump the selection to the first node sharing its key on every
|
|
7397
|
+
* lazy load. Only nodes that left the tree are re-keyed (or go pending).
|
|
7398
|
+
*/
|
|
7399
|
+
function reconcileHyperbolicSelection(selected, pending, index, canSelect, single) {
|
|
7400
|
+
if (selected.size === 0 && pending.size === 0) return {
|
|
7401
|
+
selected,
|
|
7402
|
+
pending,
|
|
7403
|
+
restored: false,
|
|
7404
|
+
changed: false
|
|
7405
|
+
};
|
|
7406
|
+
const next = /* @__PURE__ */ new Set();
|
|
7407
|
+
const nextPending = /* @__PURE__ */ new Set();
|
|
7408
|
+
let changed = false;
|
|
7409
|
+
for (const node of selected) {
|
|
7410
|
+
if (index.nodes.has(node)) {
|
|
7411
|
+
next.add(node);
|
|
7412
|
+
continue;
|
|
7413
|
+
}
|
|
7414
|
+
changed = true;
|
|
7415
|
+
const current = index.byKey.get(node.item.key);
|
|
7416
|
+
if (current) next.add(current);
|
|
7417
|
+
else nextPending.add(node.item.key);
|
|
7418
|
+
}
|
|
7419
|
+
let restored;
|
|
7420
|
+
for (const key of pending) {
|
|
7421
|
+
const node = index.byKey.get(key);
|
|
7422
|
+
if (node && canSelect(node)) {
|
|
7423
|
+
next.add(node);
|
|
7424
|
+
restored = node;
|
|
7425
|
+
changed = true;
|
|
7426
|
+
} else nextPending.add(key);
|
|
7427
|
+
}
|
|
7428
|
+
if (!changed) return {
|
|
7429
|
+
selected,
|
|
7430
|
+
pending,
|
|
7431
|
+
restored: false,
|
|
7432
|
+
changed: false
|
|
7433
|
+
};
|
|
7434
|
+
if (single && next.size > 0) return {
|
|
7435
|
+
selected: /* @__PURE__ */ new Set([restored ?? [...next].at(-1)]),
|
|
7436
|
+
pending: EMPTY_KEYS,
|
|
7437
|
+
restored: restored !== void 0,
|
|
7438
|
+
changed: true
|
|
7439
|
+
};
|
|
7440
|
+
return {
|
|
7441
|
+
selected: next,
|
|
7442
|
+
pending: capKeys(nextPending),
|
|
7443
|
+
restored: restored !== void 0,
|
|
7444
|
+
changed: true
|
|
7445
|
+
};
|
|
7446
|
+
}
|
|
7447
|
+
/**
|
|
7448
|
+
* Centre: a live centre whose path no longer resolves goes pending (the view
|
|
7449
|
+
* falls back to the root); one that resolves to a new id is re-seated; a
|
|
7450
|
+
* pending path that now resolves is applied.
|
|
7451
|
+
*/
|
|
7452
|
+
function reconcileHyperbolicViewState(input) {
|
|
7453
|
+
const { root } = input;
|
|
7454
|
+
let center;
|
|
7455
|
+
let pendingCenter = input.pendingCenter;
|
|
7456
|
+
if (input.focusId !== void 0) {
|
|
7457
|
+
const live = resolveCenterPath(root, input.centerPath);
|
|
7458
|
+
if (live.kind !== `found` || live.id === void 0) {
|
|
7459
|
+
pendingCenter ??= input.centerPath;
|
|
7460
|
+
center = {
|
|
7461
|
+
id: void 0,
|
|
7462
|
+
path: [],
|
|
7463
|
+
restored: false
|
|
7464
|
+
};
|
|
7465
|
+
} else if (live.id !== input.focusId) center = {
|
|
7466
|
+
id: live.id,
|
|
7467
|
+
path: input.centerPath,
|
|
7468
|
+
restored: false
|
|
7469
|
+
};
|
|
7470
|
+
}
|
|
7471
|
+
if (pendingCenter) {
|
|
7472
|
+
const resolved = resolveCenterPath(root, pendingCenter);
|
|
7473
|
+
if (resolved.kind === `found`) {
|
|
7474
|
+
center = {
|
|
7475
|
+
id: resolved.id,
|
|
7476
|
+
path: pendingCenter,
|
|
7477
|
+
restored: true
|
|
7478
|
+
};
|
|
7479
|
+
pendingCenter = void 0;
|
|
7480
|
+
}
|
|
7481
|
+
}
|
|
7482
|
+
const selection = reconcileHyperbolicSelection(input.selected, input.pendingSelected, input.index, input.canSelect, input.singleSelect);
|
|
7483
|
+
return {
|
|
7484
|
+
center,
|
|
7485
|
+
pendingCenter,
|
|
7486
|
+
selection
|
|
7487
|
+
};
|
|
7488
|
+
}
|
|
7489
|
+
/** Sanitise a JSON zoom: a finite positive number, else `undefined`. */
|
|
7490
|
+
function zoomFromJson(value) {
|
|
7491
|
+
return typeof value === `number` && Number.isFinite(value) && value > 0 ? value : void 0;
|
|
7492
|
+
}
|
|
7493
|
+
//#endregion
|
|
7494
|
+
//#region src/hyperbolic-tree/hyperbolic-view-state-binding.ts
|
|
7495
|
+
function createHyperbolicViewState(host) {
|
|
7496
|
+
const { controller } = host;
|
|
7497
|
+
/** Key path of the centre as last seen — what goes pending if it leaves the tree. */
|
|
7498
|
+
let centerPath = [];
|
|
7499
|
+
let pendingCenter;
|
|
7500
|
+
let pendingSelected = EMPTY_KEYS;
|
|
7501
|
+
let pendingFocus;
|
|
7502
|
+
/** Where keyboard / search navigation last put the cursor. */
|
|
7503
|
+
let keyboardCursor;
|
|
7504
|
+
let structureVersion = 0;
|
|
7505
|
+
let reconciledVersion = -1;
|
|
7506
|
+
let cache;
|
|
7507
|
+
const index = () => {
|
|
7508
|
+
if (cache?.version !== structureVersion) cache = {
|
|
7509
|
+
version: structureVersion,
|
|
7510
|
+
index: indexHyperbolicKeys(host.root())
|
|
7511
|
+
};
|
|
7512
|
+
return cache.index;
|
|
7513
|
+
};
|
|
7514
|
+
/** Key of the keyboard cursor, while the cursor is still where the keyboard put it. */
|
|
7515
|
+
const cursorKey = () => keyboardCursor && keyboardCursor.id === host.tickled() ? keyboardCursor.key : void 0;
|
|
7516
|
+
const applyCenter = (id, path, restored) => {
|
|
7517
|
+
centerPath = path;
|
|
7518
|
+
if (host.focusId() === id) return;
|
|
7519
|
+
host.setCenter(id, restored);
|
|
7520
|
+
controller.notify();
|
|
7521
|
+
};
|
|
7522
|
+
const reconcileTickled = (idx) => {
|
|
7523
|
+
const tickled = host.tickled();
|
|
7524
|
+
const alive = tickled !== void 0 && findHyperbolicPathById(host.root(), tickled) !== void 0;
|
|
7525
|
+
const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => alive && idx.byKey.has(key), (key) => host.visibleIdOf(key) !== void 0);
|
|
7526
|
+
pendingFocus = cursor.pending;
|
|
7527
|
+
if (cursor.clear || tickled !== void 0 && !alive) host.setTickled(void 0);
|
|
7528
|
+
const id = cursor.apply === void 0 ? void 0 : host.visibleIdOf(cursor.apply);
|
|
7529
|
+
if (cursor.apply !== void 0 && id !== void 0) {
|
|
7530
|
+
host.setTickled(id);
|
|
7531
|
+
keyboardCursor = {
|
|
7532
|
+
id,
|
|
7533
|
+
key: cursor.apply
|
|
7534
|
+
};
|
|
7535
|
+
controller.notify();
|
|
7536
|
+
}
|
|
7537
|
+
};
|
|
7538
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
7539
|
+
const reconcile = () => {
|
|
7540
|
+
reconciledVersion = structureVersion;
|
|
7541
|
+
const root = host.root();
|
|
7542
|
+
if (!root) return;
|
|
7543
|
+
const idx = index();
|
|
7544
|
+
controller.restoring(() => {
|
|
7545
|
+
const r = reconcileHyperbolicViewState({
|
|
7546
|
+
root,
|
|
7547
|
+
index: idx,
|
|
7548
|
+
focusId: host.focusId(),
|
|
7549
|
+
centerPath,
|
|
7550
|
+
pendingCenter,
|
|
7551
|
+
selected: host.selected(),
|
|
7552
|
+
pendingSelected,
|
|
7553
|
+
canSelect: host.canSelect,
|
|
7554
|
+
singleSelect: host.singleSelect()
|
|
7555
|
+
});
|
|
7556
|
+
pendingCenter = r.pendingCenter;
|
|
7557
|
+
if (r.center) applyCenter(r.center.id, r.center.path, r.center.restored);
|
|
7558
|
+
const sel = r.selection;
|
|
7559
|
+
if (sel.changed) {
|
|
7560
|
+
pendingSelected = sel.pending;
|
|
7561
|
+
const primary = host.selectedNode();
|
|
7562
|
+
host.setSelection(sel.selected, primary && sel.selected.has(primary) ? primary : [...sel.selected].at(-1), sel.restored);
|
|
7563
|
+
}
|
|
7564
|
+
reconcileTickled(idx);
|
|
7565
|
+
});
|
|
7566
|
+
};
|
|
7567
|
+
return {
|
|
7568
|
+
/** The structure changed (new root / model, invalidation, lazy load). */
|
|
7569
|
+
invalidate() {
|
|
7570
|
+
structureVersion++;
|
|
7571
|
+
},
|
|
7572
|
+
/** Run in `willUpdate`. `centerGone` — the centre vanished through in-place mutation. */
|
|
7573
|
+
reconcileIfNeeded(centerGone) {
|
|
7574
|
+
if (centerGone) structureVersion++;
|
|
7575
|
+
if (reconciledVersion !== structureVersion) reconcile();
|
|
7576
|
+
},
|
|
7577
|
+
/** An explicit centre change: supersedes a saved one still waiting. */
|
|
7578
|
+
centred(id) {
|
|
7579
|
+
pendingCenter = void 0;
|
|
7580
|
+
centerPath = centerPathOf(host.root(), id);
|
|
7581
|
+
controller.notify();
|
|
7582
|
+
},
|
|
7583
|
+
/** Selection changed — explicit changes supersede a saved selection still waiting. */
|
|
7584
|
+
selectionChanged() {
|
|
7585
|
+
if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
|
|
7586
|
+
controller.notify();
|
|
7587
|
+
},
|
|
7588
|
+
clearPendingSelection() {
|
|
7589
|
+
pendingSelected = EMPTY_KEYS;
|
|
7590
|
+
},
|
|
7591
|
+
/** The cursor moved by keyboard / search navigation. */
|
|
7592
|
+
keyboardCursorMoved(id, key) {
|
|
7593
|
+
keyboardCursor = {
|
|
7594
|
+
id,
|
|
7595
|
+
key
|
|
7596
|
+
};
|
|
7597
|
+
pendingFocus = void 0;
|
|
7598
|
+
controller.notify();
|
|
7599
|
+
},
|
|
7600
|
+
/** A user-visible change with no pending facet (e.g. a zoom gesture). */
|
|
7601
|
+
notify() {
|
|
7602
|
+
controller.notify();
|
|
7603
|
+
},
|
|
7604
|
+
get(options) {
|
|
7605
|
+
const withPending = options?.includePending ?? true;
|
|
7606
|
+
return controller.shape({
|
|
7607
|
+
selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
|
|
7608
|
+
focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
|
|
7609
|
+
center: [...(withPending ? pendingCenter : void 0) ?? centerPath],
|
|
7610
|
+
zoom: host.zoom()
|
|
7611
|
+
}, options);
|
|
7612
|
+
},
|
|
7613
|
+
set(state) {
|
|
7614
|
+
if (!controller.accepts(state)) return;
|
|
7615
|
+
controller.restoring(() => {
|
|
7616
|
+
if (state.selected) {
|
|
7617
|
+
pendingSelected = capKeys(new Set(state.selected));
|
|
7618
|
+
if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
|
|
7619
|
+
}
|
|
7620
|
+
if (state.focus !== void 0) pendingFocus = state.focus;
|
|
7621
|
+
const zoom = zoomFromJson(state.zoom);
|
|
7622
|
+
if (zoom !== void 0 && zoom !== host.zoom()) {
|
|
7623
|
+
host.setZoom(zoom);
|
|
7624
|
+
controller.notify();
|
|
7625
|
+
}
|
|
7626
|
+
const center = pathFromJson(state.center);
|
|
7627
|
+
if (center) pendingCenter = center;
|
|
7628
|
+
reconcile();
|
|
7629
|
+
if (center && pendingCenter) applyCenter(void 0, [], true);
|
|
7630
|
+
});
|
|
7631
|
+
},
|
|
7632
|
+
clearPending() {
|
|
7633
|
+
pendingSelected = EMPTY_KEYS;
|
|
7634
|
+
pendingFocus = void 0;
|
|
7635
|
+
pendingCenter = void 0;
|
|
7636
|
+
},
|
|
7637
|
+
/** Load the next unloaded ancestor on a pending centre path (one load at a time). */
|
|
7638
|
+
continueRestoreLoads() {
|
|
7639
|
+
if (!pendingCenter || !host.restoreLoads()) return;
|
|
7640
|
+
const resolved = resolveCenterPath(host.root(), pendingCenter);
|
|
7641
|
+
if (resolved.kind === `load`) host.load(resolved.node, resolved.depth);
|
|
7642
|
+
}
|
|
7643
|
+
};
|
|
7644
|
+
}
|
|
7645
|
+
//#endregion
|
|
7646
|
+
//#region src/hyperbolic-tree/hyperbolictree-styles.ts
|
|
7647
|
+
/** Shadow-DOM styles for `ixfx-hyperbolictree-list`. */
|
|
7648
|
+
const hyperbolicTreeStyles = css`
|
|
7649
|
+
:host {
|
|
7650
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
7651
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
7652
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
7653
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
7654
|
+
/* Real screen px — converted to SVG units from the element's rendered size. */
|
|
7655
|
+
--hyperbolictree-font-size: 13px;
|
|
7656
|
+
--hyperbolictree-box-color: var(--hyperbolictree-node-color);
|
|
7657
|
+
--hyperbolictree-box-text: var(--surface-4-text);
|
|
7658
|
+
--hyperbolictree-box-border: var(--surface-2);
|
|
7659
|
+
--hyperbolictree-branch-hue-start: 250;
|
|
7660
|
+
--hyperbolictree-branch-chroma: 0.13;
|
|
7661
|
+
--hyperbolictree-branch-lightness: 62%;
|
|
7662
|
+
--hyperbolictree-branch-mix: 55%;
|
|
7663
|
+
|
|
7664
|
+
display: block;
|
|
7665
|
+
position: relative;
|
|
7666
|
+
overflow: hidden;
|
|
7667
|
+
outline: none;
|
|
7668
|
+
overscroll-behavior: contain;
|
|
7669
|
+
}
|
|
7670
|
+
|
|
7671
|
+
/*
|
|
7672
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
7673
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
7674
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
7675
|
+
* sized to the whole box.
|
|
7676
|
+
*/
|
|
7677
|
+
:host(:focus),
|
|
7678
|
+
:host(:focus-visible) {
|
|
7679
|
+
outline: none;
|
|
7680
|
+
}
|
|
7681
|
+
|
|
7682
|
+
.hyperbolictree {
|
|
7683
|
+
display: block;
|
|
7684
|
+
width: 100%;
|
|
7685
|
+
height: 100%;
|
|
7686
|
+
outline: none;
|
|
7687
|
+
touch-action: none;
|
|
7688
|
+
overscroll-behavior: contain;
|
|
7689
|
+
}
|
|
7690
|
+
|
|
7691
|
+
.disk-boundary {
|
|
7692
|
+
fill: none;
|
|
7693
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
7694
|
+
stroke-width: 1;
|
|
7695
|
+
}
|
|
7696
|
+
|
|
7697
|
+
.edge {
|
|
7698
|
+
fill: none;
|
|
7699
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
7700
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
7701
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
7702
|
+
/* Constant on-screen width, however far the disk is zoomed. */
|
|
7703
|
+
vector-effect: non-scaling-stroke;
|
|
7704
|
+
}
|
|
7705
|
+
|
|
7706
|
+
.node {
|
|
7707
|
+
cursor: pointer;
|
|
7708
|
+
}
|
|
7709
|
+
|
|
7710
|
+
/*
|
|
7711
|
+
* color-by-branch: each top-level branch gets an evenly-spaced hue,
|
|
7712
|
+
* mixed into the normal node/box/edge colour (rather than replacing it)
|
|
7713
|
+
* so it stays in keeping with — and legible in — the current theme.
|
|
7714
|
+
* Only sets the base colour vars, so selected/tickled states still win.
|
|
7715
|
+
*/
|
|
7716
|
+
:host([color-by-branch]) .node,
|
|
7717
|
+
:host([color-by-branch]) .edge {
|
|
7718
|
+
--_branch-color: oklch(
|
|
7719
|
+
var(--hyperbolictree-branch-lightness)
|
|
7720
|
+
var(--hyperbolictree-branch-chroma)
|
|
7721
|
+
calc(var(--hyperbolictree-branch-hue-start) + var(--branch-index) * 360 / var(--branch-count))
|
|
7722
|
+
);
|
|
7723
|
+
}
|
|
7724
|
+
|
|
7725
|
+
:host([color-by-branch]) .node {
|
|
7726
|
+
--_node-base: color-mix(in oklab, var(--hyperbolictree-node-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
7727
|
+
--_box-base: color-mix(in oklab, var(--hyperbolictree-box-color), var(--_branch-color) var(--hyperbolictree-branch-mix));
|
|
7728
|
+
}
|
|
7729
|
+
|
|
7730
|
+
:host([color-by-branch]) .edge {
|
|
7731
|
+
stroke: color-mix(in oklab, var(--hyperbolictree-edge-color), var(--_branch-color) 50%);
|
|
7732
|
+
}
|
|
7733
|
+
|
|
7734
|
+
.node-hit {
|
|
7735
|
+
fill: transparent;
|
|
7736
|
+
pointer-events: all;
|
|
7737
|
+
}
|
|
7738
|
+
|
|
7739
|
+
.node-dot {
|
|
7740
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
7741
|
+
fill: color-mix(in oklab, var(--_node-base, var(--hyperbolictree-node-color)), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
7742
|
+
stroke: var(--surface-2);
|
|
7743
|
+
stroke-width: 1;
|
|
7744
|
+
pointer-events: none;
|
|
7745
|
+
}
|
|
7746
|
+
|
|
7747
|
+
.node-box {
|
|
7748
|
+
fill: color-mix(in oklab, var(--_box-base, var(--hyperbolictree-box-color)), var(--accent) calc(var(--node-weight, 0.5) * 25%));
|
|
7749
|
+
stroke: var(--hyperbolictree-box-border);
|
|
7750
|
+
stroke-width: 1.5;
|
|
7751
|
+
/* Keep the border crisp however far the box has been scaled down. */
|
|
7752
|
+
vector-effect: non-scaling-stroke;
|
|
7753
|
+
}
|
|
7754
|
+
|
|
7755
|
+
/* Text-less stand-in for a box too small, or too crowded, to label. */
|
|
7756
|
+
.node-box.marker {
|
|
7757
|
+
stroke-width: 1;
|
|
7758
|
+
}
|
|
7759
|
+
|
|
7760
|
+
/* Pure user-space font-size; set per render from --hyperbolictree-font-size. */
|
|
7761
|
+
.hyperbolictree-label {
|
|
7762
|
+
fill: var(--hyperbolictree-node-text);
|
|
7763
|
+
cursor: pointer;
|
|
7764
|
+
user-select: none;
|
|
7765
|
+
}
|
|
7766
|
+
|
|
7767
|
+
.box-label {
|
|
7768
|
+
fill: var(--hyperbolictree-box-text);
|
|
7769
|
+
pointer-events: none;
|
|
7770
|
+
}
|
|
7771
|
+
|
|
7772
|
+
/*
|
|
7773
|
+
* State fills come paired with their theme text tokens: a box label sits
|
|
7774
|
+
* on the state fill itself, so e.g. --item-bg-selected needs
|
|
7775
|
+
* --item-text-selected on top, not the resting box text colour.
|
|
7776
|
+
*/
|
|
7777
|
+
.node.tickled .node-dot,
|
|
7778
|
+
.node.tickled .node-box {
|
|
7779
|
+
fill: var(--item-bg-tickled);
|
|
7780
|
+
}
|
|
7781
|
+
|
|
7782
|
+
.node.tickled .box-label {
|
|
7783
|
+
fill: var(--item-text-tickled);
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
.node.selected .node-dot,
|
|
7787
|
+
.node.selected .node-box {
|
|
7788
|
+
fill: var(--item-bg-selected);
|
|
7789
|
+
}
|
|
7790
|
+
|
|
7791
|
+
.node.selected .box-label {
|
|
7792
|
+
fill: var(--item-text-selected);
|
|
7793
|
+
}
|
|
7794
|
+
|
|
7795
|
+
.node.selected.tickled .node-dot,
|
|
7796
|
+
.node.selected.tickled .node-box {
|
|
7797
|
+
fill: var(--item-bg-selected-tickled);
|
|
7798
|
+
}
|
|
7799
|
+
|
|
7800
|
+
.node.focused .node-dot,
|
|
7801
|
+
.node.focused .node-box {
|
|
7802
|
+
stroke: var(--accent);
|
|
7803
|
+
stroke-width: 3;
|
|
7804
|
+
}
|
|
7805
|
+
|
|
7806
|
+
/* A hovered label may sit over busy neighbours — give it a halo so it always reads. */
|
|
7807
|
+
.node.tickled .hyperbolictree-label:not(.box-label) {
|
|
7808
|
+
paint-order: stroke;
|
|
7809
|
+
stroke: var(--surface-2);
|
|
7810
|
+
stroke-width: 4px;
|
|
7811
|
+
stroke-linejoin: round;
|
|
7812
|
+
}
|
|
7813
|
+
|
|
7814
|
+
.node.filtered {
|
|
7815
|
+
opacity: 0.3;
|
|
7816
|
+
}
|
|
7817
|
+
|
|
7818
|
+
.node.loading .node-dot,
|
|
7819
|
+
.node.loading .node-box {
|
|
7820
|
+
opacity: 0.6;
|
|
7821
|
+
}
|
|
7822
|
+
|
|
7823
|
+
|
|
7824
|
+
/* Child preview: a clickable list of a crowded node's children. */
|
|
7825
|
+
.child-preview {
|
|
7826
|
+
position: absolute;
|
|
7827
|
+
z-index: 1;
|
|
7828
|
+
min-width: 8rem;
|
|
7829
|
+
max-width: min(18rem, 60%);
|
|
7830
|
+
max-height: 45%;
|
|
7831
|
+
overflow: auto;
|
|
7832
|
+
padding: var(--space-xs, 4px);
|
|
7833
|
+
background: var(--surface-3);
|
|
7834
|
+
color: var(--surface-3-text);
|
|
7835
|
+
border: var(--border-subtle);
|
|
7836
|
+
border-radius: var(--radius-m, 6px);
|
|
7837
|
+
box-shadow: var(--shadow-m);
|
|
7838
|
+
font-size: var(--hyperbolictree-font-size);
|
|
7839
|
+
/* Beside the node, never over it, with the dot level with the popover's middle. */
|
|
7840
|
+
transform: translate(14px, -50%);
|
|
7841
|
+
}
|
|
7842
|
+
|
|
7843
|
+
.child-preview.left {
|
|
7844
|
+
transform: translate(calc(-100% - 14px), -50%);
|
|
7845
|
+
}
|
|
7846
|
+
|
|
7847
|
+
.child-preview.below {
|
|
7848
|
+
transform: translate(-50%, 14px);
|
|
7849
|
+
}
|
|
7850
|
+
|
|
7851
|
+
.child-preview.above {
|
|
7852
|
+
transform: translate(-50%, calc(-100% - 14px));
|
|
7853
|
+
}
|
|
7854
|
+
|
|
7855
|
+
.child-preview-title {
|
|
7856
|
+
display: flex;
|
|
7857
|
+
align-items: center;
|
|
7858
|
+
gap: 4px;
|
|
7859
|
+
padding: 2px 6px 4px;
|
|
7860
|
+
font-weight: 600;
|
|
7861
|
+
}
|
|
7862
|
+
|
|
7863
|
+
.child-preview-title span {
|
|
7864
|
+
overflow: hidden;
|
|
7865
|
+
text-overflow: ellipsis;
|
|
7866
|
+
white-space: nowrap;
|
|
7867
|
+
}
|
|
7868
|
+
|
|
7869
|
+
.child-preview button.child-preview-up {
|
|
7870
|
+
display: inline-flex;
|
|
7871
|
+
flex: none;
|
|
7872
|
+
width: auto;
|
|
7873
|
+
padding: 2px;
|
|
7874
|
+
}
|
|
7875
|
+
|
|
7876
|
+
.child-preview ul {
|
|
7877
|
+
margin: 0;
|
|
7878
|
+
padding: 0;
|
|
7879
|
+
list-style: none;
|
|
7880
|
+
}
|
|
7881
|
+
|
|
7882
|
+
.child-preview button {
|
|
7883
|
+
display: block;
|
|
7884
|
+
width: 100%;
|
|
7885
|
+
padding: 2px 6px;
|
|
7886
|
+
border: none;
|
|
7887
|
+
border-radius: var(--radius-s, 4px);
|
|
7888
|
+
background: none;
|
|
7889
|
+
color: inherit;
|
|
7890
|
+
font: inherit;
|
|
7891
|
+
text-align: start;
|
|
7892
|
+
overflow: hidden;
|
|
7893
|
+
text-overflow: ellipsis;
|
|
7894
|
+
white-space: nowrap;
|
|
7895
|
+
cursor: pointer;
|
|
7896
|
+
}
|
|
7897
|
+
|
|
7898
|
+
.child-preview button:hover {
|
|
7899
|
+
background: var(--item-bg-tickled);
|
|
7900
|
+
color: var(--item-text-tickled);
|
|
7901
|
+
}
|
|
7902
|
+
|
|
7903
|
+
.child-preview-more {
|
|
7904
|
+
padding: 2px 6px;
|
|
7905
|
+
color: var(--surface-muted-text);
|
|
7906
|
+
}
|
|
7907
|
+
`;
|
|
7908
|
+
//#endregion
|
|
7448
7909
|
//#region src/hyperbolic-tree/keyboard.ts
|
|
7449
7910
|
const unhandled = {
|
|
7450
7911
|
action: { kind: `none` },
|
|
@@ -7543,6 +8004,11 @@ function createLazyChildLoader(source, onChange) {
|
|
|
7543
8004
|
if (!model && !loadChildren) return;
|
|
7544
8005
|
for (const { node, depth } of visible) if (node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
7545
8006
|
},
|
|
8007
|
+
/** Start loading `node`'s children unless loaded or already loading (view-state restore). */
|
|
8008
|
+
loadOne(node, depth) {
|
|
8009
|
+
const { model, loadChildren } = source();
|
|
8010
|
+
if ((model || loadChildren) && node.children === void 0 && !pending.has(node)) load(node, depth);
|
|
8011
|
+
},
|
|
7546
8012
|
isPending: (node) => pending.has(node),
|
|
7547
8013
|
abortAll() {
|
|
7548
8014
|
for (const aborter of aborters.values()) aborter.abort();
|
|
@@ -8063,6 +8529,35 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8063
8529
|
this.disableKeyboardNavigation = false;
|
|
8064
8530
|
this.registry = void 0;
|
|
8065
8531
|
this._focusId = void 0;
|
|
8532
|
+
this.restoreLoads = true;
|
|
8533
|
+
this.#viewState = createHyperbolicViewState({
|
|
8534
|
+
controller: new ViewStateController(this, {
|
|
8535
|
+
kind: `hyperbolictree-list`,
|
|
8536
|
+
capture: () => this.getViewState()
|
|
8537
|
+
}),
|
|
8538
|
+
root: () => this.#effectiveRoot,
|
|
8539
|
+
focusId: () => this._focusId,
|
|
8540
|
+
setCenter: (id, restored) => this.#restoreCenter(id, restored),
|
|
8541
|
+
selected: () => this._selectedNodes,
|
|
8542
|
+
selectedNode: () => this.selectedNode,
|
|
8543
|
+
canSelect: (node) => this.#canSelect(node),
|
|
8544
|
+
singleSelect: () => this.interactionMode === `implicit`,
|
|
8545
|
+
setSelection: (next, primary, fire) => {
|
|
8546
|
+
const prev = this._selectedNodes;
|
|
8547
|
+
this._selectedNodes = new Set(next);
|
|
8548
|
+
this.selectedNode = primary;
|
|
8549
|
+
if (fire) this.#dispatchSelect(prev);
|
|
8550
|
+
},
|
|
8551
|
+
tickled: () => this.tickled.tickledId,
|
|
8552
|
+
setTickled: (id) => id === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(id),
|
|
8553
|
+
visibleIdOf: (key) => this.#layout().nodes.find((n) => n.key === key)?.id,
|
|
8554
|
+
zoom: () => this.zoom,
|
|
8555
|
+
setZoom: (zoom) => {
|
|
8556
|
+
this.zoom = zoom;
|
|
8557
|
+
},
|
|
8558
|
+
restoreLoads: () => this.restoreLoads,
|
|
8559
|
+
load: (node, depth) => this.#lazy.loadOne(node, depth)
|
|
8560
|
+
});
|
|
8066
8561
|
this._viewOverride = void 0;
|
|
8067
8562
|
this._focusRotation = 0;
|
|
8068
8563
|
this._zoomOffset = {
|
|
@@ -8084,7 +8579,10 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8084
8579
|
this.#lazy = createLazyChildLoader(() => ({
|
|
8085
8580
|
model: this.model,
|
|
8086
8581
|
loadChildren: this.loadChildren
|
|
8087
|
-
}), () =>
|
|
8582
|
+
}), () => {
|
|
8583
|
+
this.#viewState.invalidate();
|
|
8584
|
+
this.requestUpdate();
|
|
8585
|
+
});
|
|
8088
8586
|
this.#lastLabelIds = /* @__PURE__ */ new Set();
|
|
8089
8587
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8090
8588
|
labelSelector: `.hyperbolictree-label`,
|
|
@@ -8131,6 +8629,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8131
8629
|
},
|
|
8132
8630
|
zoomState: () => this.#zoomState(),
|
|
8133
8631
|
setZoomState: (state) => {
|
|
8632
|
+
if (state.zoom !== this.zoom) this.#viewState.notify();
|
|
8134
8633
|
this.zoom = state.zoom;
|
|
8135
8634
|
this._zoomOffset = state.offset;
|
|
8136
8635
|
},
|
|
@@ -8153,6 +8652,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8153
8652
|
if (this._focusId === void 0) return this.#effectiveRoot;
|
|
8154
8653
|
return findHyperbolicPathById(this.#effectiveRoot, this._focusId)?.at(-1) ?? this.#effectiveRoot;
|
|
8155
8654
|
}
|
|
8655
|
+
#viewState;
|
|
8156
8656
|
#preview;
|
|
8157
8657
|
#lazy;
|
|
8158
8658
|
/** Labels shown in the last render — what the child preview judges legibility by. */
|
|
@@ -8260,46 +8760,30 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8260
8760
|
this.#unregisterSearchCommands?.();
|
|
8261
8761
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
8262
8762
|
}
|
|
8763
|
+
willUpdate(changedProps) {
|
|
8764
|
+
if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
|
|
8765
|
+
this.#viewState.reconcileIfNeeded(this._focusId !== void 0 && !findHyperbolicPathById(this.#effectiveRoot, this._focusId));
|
|
8766
|
+
}
|
|
8263
8767
|
updated(changedProps) {
|
|
8264
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
8265
8768
|
if (changedProps.has(`model`)) {
|
|
8266
8769
|
this.#modelUnsubscribe?.();
|
|
8267
8770
|
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
|
-
}
|
|
8771
|
+
if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
8772
|
+
if (event.type === `root`) this.#onDataReset();
|
|
8773
|
+
else if (event.type === `invalidate`) this.#viewState.invalidate();
|
|
8774
|
+
this.requestUpdate();
|
|
8775
|
+
});
|
|
8277
8776
|
}
|
|
8278
|
-
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
8777
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) this.clearSelection();
|
|
8279
8778
|
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
8779
|
this.#lazy.ensure(this.#layout().nodes);
|
|
8780
|
+
this.#viewState.continueRestoreLoads();
|
|
8286
8781
|
}
|
|
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
|
-
};
|
|
8782
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
8783
|
+
#onDataReset() {
|
|
8297
8784
|
this._previewId = void 0;
|
|
8298
8785
|
this.#gestures.cancel();
|
|
8299
|
-
|
|
8300
|
-
cancelAnimationFrame(this.#animFrame);
|
|
8301
|
-
this.#animFrame = void 0;
|
|
8302
|
-
}
|
|
8786
|
+
this.#viewState.invalidate();
|
|
8303
8787
|
}
|
|
8304
8788
|
#canSelect(node) {
|
|
8305
8789
|
if (this.selectionFilter === `none`) return false;
|
|
@@ -8312,6 +8796,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8312
8796
|
this.selectedNode = void 0;
|
|
8313
8797
|
}
|
|
8314
8798
|
#dispatchSelect(previous) {
|
|
8799
|
+
this.#viewState.selectionChanged();
|
|
8315
8800
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
8316
8801
|
detail: {
|
|
8317
8802
|
selected: new Set(this._selectedNodes),
|
|
@@ -8339,6 +8824,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8339
8824
|
this.requestUpdate();
|
|
8340
8825
|
}
|
|
8341
8826
|
clearSelection() {
|
|
8827
|
+
this.#viewState.clearPendingSelection();
|
|
8342
8828
|
if (this._selectedNodes.size === 0) return;
|
|
8343
8829
|
const prev = new Set(this._selectedNodes);
|
|
8344
8830
|
this.#clearSelectionInternal();
|
|
@@ -8384,6 +8870,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8384
8870
|
const previousNode = currentId === void 0 ? void 0 : findHyperbolicPathById(root, currentId)?.at(-1);
|
|
8385
8871
|
const fromView = this.#viewFromFrameOf(targetId, currentId);
|
|
8386
8872
|
this._focusId = targetId;
|
|
8873
|
+
this.#viewState.centred(targetId);
|
|
8387
8874
|
this.#gestures.cancel();
|
|
8388
8875
|
this._previewId = void 0;
|
|
8389
8876
|
this.#animateTo(fromView);
|
|
@@ -8452,7 +8939,7 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8452
8939
|
#focusAndMaybeSelect(node) {
|
|
8453
8940
|
this.focusOn(node);
|
|
8454
8941
|
const id = this.#idOf(node);
|
|
8455
|
-
if (id !== void 0) this
|
|
8942
|
+
if (id !== void 0) this.#setKeyboardCursor(id, node);
|
|
8456
8943
|
if (this.interactionMode === `implicit`) this.select(node);
|
|
8457
8944
|
}
|
|
8458
8945
|
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
@@ -8490,10 +8977,15 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8490
8977
|
const id = node ? this.#idOf(node) : void 0;
|
|
8491
8978
|
if (!node || id === void 0) return false;
|
|
8492
8979
|
if (!this.#layout().nodes.some((n) => n.id === id)) this.focusOn(node);
|
|
8493
|
-
this
|
|
8980
|
+
this.#setKeyboardCursor(id, node);
|
|
8494
8981
|
this.requestUpdate();
|
|
8495
8982
|
return true;
|
|
8496
8983
|
}
|
|
8984
|
+
/** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
|
|
8985
|
+
#setKeyboardCursor(id, node) {
|
|
8986
|
+
this.tickled.setTickled(id);
|
|
8987
|
+
this.#viewState.keyboardCursorMoved(id, node.item.key);
|
|
8988
|
+
}
|
|
8497
8989
|
/**
|
|
8498
8990
|
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
8499
8991
|
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
@@ -8506,6 +8998,49 @@ let HyperbolicTreeListElement = class HyperbolicTreeListElement extends LitEleme
|
|
|
8506
8998
|
depth: n.depth
|
|
8507
8999
|
}));
|
|
8508
9000
|
}
|
|
9001
|
+
/**
|
|
9002
|
+
* Snapshot selection, centred node, keyboard cursor and zoom as plain JSON,
|
|
9003
|
+
* keyed by `item.key`. Includes pending state for keys not currently loaded.
|
|
9004
|
+
*/
|
|
9005
|
+
getViewState(options) {
|
|
9006
|
+
return this.#viewState.get(options);
|
|
9007
|
+
}
|
|
9008
|
+
/**
|
|
9009
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
9010
|
+
* that aren't loaded yet are applied as they appear — a deep `center` loads
|
|
9011
|
+
* its ancestors via `loadChildren` / `model` when `restoreLoads`.
|
|
9012
|
+
*/
|
|
9013
|
+
setViewState(state) {
|
|
9014
|
+
this.#viewState.set(state);
|
|
9015
|
+
this.requestUpdate();
|
|
9016
|
+
}
|
|
9017
|
+
clearPendingViewState() {
|
|
9018
|
+
this.#viewState.clearPending();
|
|
9019
|
+
}
|
|
9020
|
+
/** Centre set by view-state reconcile: snaps (no animation); `restored` fires `focus`. */
|
|
9021
|
+
#restoreCenter(id, restored) {
|
|
9022
|
+
const previous = this.focusedNode;
|
|
9023
|
+
this.#cancelAnimation();
|
|
9024
|
+
this._focusId = id;
|
|
9025
|
+
this._viewOverride = void 0;
|
|
9026
|
+
this._zoomOffset = {
|
|
9027
|
+
x: 0,
|
|
9028
|
+
y: 0
|
|
9029
|
+
};
|
|
9030
|
+
if (!restored) {
|
|
9031
|
+
this._focusRotation = 0;
|
|
9032
|
+
return;
|
|
9033
|
+
}
|
|
9034
|
+
const node = this.focusedNode;
|
|
9035
|
+
if (node) this.dispatchEvent(new CustomEvent(`focus`, {
|
|
9036
|
+
detail: {
|
|
9037
|
+
node,
|
|
9038
|
+
previous
|
|
9039
|
+
},
|
|
9040
|
+
bubbles: true,
|
|
9041
|
+
composed: true
|
|
9042
|
+
}));
|
|
9043
|
+
}
|
|
8509
9044
|
#matchesFilter(node, depth) {
|
|
8510
9045
|
if (!this.filterPredicate) return true;
|
|
8511
9046
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -8791,6 +9326,7 @@ __decorate([property({
|
|
|
8791
9326
|
reflect: true
|
|
8792
9327
|
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
8793
9328
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusId", void 0);
|
|
9329
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "restoreLoads", void 0);
|
|
8794
9330
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_viewOverride", void 0);
|
|
8795
9331
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusRotation", void 0);
|
|
8796
9332
|
__decorate([state()], HyperbolicTreeListElement.prototype, "_zoomOffset", void 0);
|
|
@@ -8825,6 +9361,30 @@ function findIciclePath(root, key) {
|
|
|
8825
9361
|
}
|
|
8826
9362
|
}
|
|
8827
9363
|
/**
|
|
9364
|
+
* Follow `keys` from `root`; a leading key equal to the root's own key
|
|
9365
|
+
* matches the root itself. Returns `undefined` when a key isn't found.
|
|
9366
|
+
*/
|
|
9367
|
+
function resolveIcicleKeyPath(root, keys) {
|
|
9368
|
+
let current = root;
|
|
9369
|
+
for (const key of keys) {
|
|
9370
|
+
const found = key === current?.item.key ? current : current?.children?.find((n) => n.item.key === key);
|
|
9371
|
+
if (!found) return void 0;
|
|
9372
|
+
current = found;
|
|
9373
|
+
}
|
|
9374
|
+
return current;
|
|
9375
|
+
}
|
|
9376
|
+
/** Key path (depth-first) to the first node among `nodes` matching `predicate`. */
|
|
9377
|
+
function findIcicleKeyPath(nodes, predicate, prefix = []) {
|
|
9378
|
+
for (const node of nodes) {
|
|
9379
|
+
const path = [...prefix, node.item.key];
|
|
9380
|
+
if (predicate(node)) return path;
|
|
9381
|
+
if (node.children?.length) {
|
|
9382
|
+
const hit = findIcicleKeyPath(node.children, predicate, path);
|
|
9383
|
+
if (hit) return hit;
|
|
9384
|
+
}
|
|
9385
|
+
}
|
|
9386
|
+
}
|
|
9387
|
+
/**
|
|
8828
9388
|
* Compute the visible rectangles for an icicle view.
|
|
8829
9389
|
*
|
|
8830
9390
|
* Bands start at the zoom node's **children** (the zoom node itself is shown in
|
|
@@ -8878,6 +9438,259 @@ function computeIcicleLayout(root, options = {}) {
|
|
|
8878
9438
|
return rects;
|
|
8879
9439
|
}
|
|
8880
9440
|
//#endregion
|
|
9441
|
+
//#region src/icicle-tree/icicle-view-state.ts
|
|
9442
|
+
/**
|
|
9443
|
+
* - `found` — every key matched: `nodes[i]` is the node for `keys[i]`.
|
|
9444
|
+
* - `load` — `node` (at `depth`; root's children are `0`) must load its
|
|
9445
|
+
* children before the path can continue.
|
|
9446
|
+
* - `missing` — a key isn't there (yet), or the next step can't load.
|
|
9447
|
+
*/
|
|
9448
|
+
/** First node (depth-first) for each key in the loaded tree. */
|
|
9449
|
+
function indexNodesByKey(root) {
|
|
9450
|
+
const out = /* @__PURE__ */ new Map();
|
|
9451
|
+
const visit = (node) => {
|
|
9452
|
+
if (!out.has(node.item.key)) out.set(node.item.key, node);
|
|
9453
|
+
for (const child of node.children ?? []) visit(child);
|
|
9454
|
+
};
|
|
9455
|
+
if (root) visit(root);
|
|
9456
|
+
return out;
|
|
9457
|
+
}
|
|
9458
|
+
/**
|
|
9459
|
+
* Nodes along a pending zoom path (`[]` = full tree), or `undefined` while it
|
|
9460
|
+
* can't resolve. Keys are unique tree-wide, so a zoom node that has moved is
|
|
9461
|
+
* still found by its key.
|
|
9462
|
+
*/
|
|
9463
|
+
function resolveZoomPath$1(root, path, nodeByKey) {
|
|
9464
|
+
const found = resolveKeyPath(root, path, () => false);
|
|
9465
|
+
if (found.status === `found`) return found.nodes;
|
|
9466
|
+
const key = path.at(-1);
|
|
9467
|
+
return key !== void 0 && nodeByKey(key) ? findIciclePath(root, key)?.slice(1) : void 0;
|
|
9468
|
+
}
|
|
9469
|
+
/** Pixel scroll of a scrolling host. Rects aren't a linear list, so there's no anchor. */
|
|
9470
|
+
function captureHostScroll(el) {
|
|
9471
|
+
return el.scrollLeft === 0 ? { top: el.scrollTop } : {
|
|
9472
|
+
top: el.scrollTop,
|
|
9473
|
+
left: el.scrollLeft
|
|
9474
|
+
};
|
|
9475
|
+
}
|
|
9476
|
+
/** Returns `true` once the saved position is fully reached (else retry after the next render). */
|
|
9477
|
+
function applyHostScroll(el, scroll) {
|
|
9478
|
+
const top = scrollToOffset(el, scroll.top);
|
|
9479
|
+
const left = scroll.left === void 0 || scrollToOffset(el, scroll.left, `horizontal`);
|
|
9480
|
+
return top && left;
|
|
9481
|
+
}
|
|
9482
|
+
//#endregion
|
|
9483
|
+
//#region src/icicle-tree/keyboard.ts
|
|
9484
|
+
/**
|
|
9485
|
+
* Rect the cursor moves to for `key`, or `undefined` when the key isn't a
|
|
9486
|
+
* navigation key or there's nowhere to go. With no current rect, any
|
|
9487
|
+
* navigation key lands on the first rect.
|
|
9488
|
+
*/
|
|
9489
|
+
function icicleKeyTarget(rects, current, key) {
|
|
9490
|
+
if (rects.length === 0) return void 0;
|
|
9491
|
+
if (key === `Home`) return rects[0];
|
|
9492
|
+
if (key === `End`) return rects.at(-1);
|
|
9493
|
+
if (![
|
|
9494
|
+
`ArrowLeft`,
|
|
9495
|
+
`ArrowRight`,
|
|
9496
|
+
`ArrowUp`,
|
|
9497
|
+
`ArrowDown`
|
|
9498
|
+
].includes(key)) return void 0;
|
|
9499
|
+
if (!current) return rects[0];
|
|
9500
|
+
switch (key) {
|
|
9501
|
+
case `ArrowLeft`:
|
|
9502
|
+
case `ArrowRight`: {
|
|
9503
|
+
const dir = key === `ArrowRight` ? 1 : -1;
|
|
9504
|
+
const siblings = rects.filter((r) => r.relativeDepth === current.relativeDepth && r.parentKey === current.parentKey);
|
|
9505
|
+
return siblings[siblings.findIndex((r) => r.node.item.key === current.node.item.key) + dir] ?? (dir > 0 ? siblings[0] : siblings.at(-1));
|
|
9506
|
+
}
|
|
9507
|
+
case `ArrowUp`: return (current.parentKey === void 0 ? void 0 : rects.find((r) => r.node.item.key === current.parentKey)) ?? rects[0];
|
|
9508
|
+
default: return rects.find((r) => r.parentKey === current.node.item.key);
|
|
9509
|
+
}
|
|
9510
|
+
}
|
|
9511
|
+
//#endregion
|
|
9512
|
+
//#region src/icicle-tree/styles.ts
|
|
9513
|
+
/** Shadow-DOM styles for `ixfx-icicletree-list`. */
|
|
9514
|
+
const icicleTreeStyles = css`
|
|
9515
|
+
:host {
|
|
9516
|
+
--icicletree-band-height: 44px;
|
|
9517
|
+
--icicletree-band-gap: 2px;
|
|
9518
|
+
--icicletree-rect-gap: 2px;
|
|
9519
|
+
--icicletree-rect-bg: var(--surface-4);
|
|
9520
|
+
--icicletree-rect-text: var(--surface-4-text);
|
|
9521
|
+
--icicletree-font-size: var(--text-s);
|
|
9522
|
+
|
|
9523
|
+
display: block;
|
|
9524
|
+
overflow: auto;
|
|
9525
|
+
}
|
|
9526
|
+
|
|
9527
|
+
.icicle {
|
|
9528
|
+
display: flex;
|
|
9529
|
+
gap: var(--icicletree-band-gap);
|
|
9530
|
+
outline: none;
|
|
9531
|
+
}
|
|
9532
|
+
|
|
9533
|
+
.icicle.horizontal {
|
|
9534
|
+
flex-direction: column;
|
|
9535
|
+
}
|
|
9536
|
+
|
|
9537
|
+
.icicle.vertical {
|
|
9538
|
+
flex-direction: row;
|
|
9539
|
+
min-height: 200px;
|
|
9540
|
+
}
|
|
9541
|
+
|
|
9542
|
+
.breadcrumb {
|
|
9543
|
+
display: flex;
|
|
9544
|
+
align-items: center;
|
|
9545
|
+
padding: var(--space-xs) var(--space-s);
|
|
9546
|
+
background: var(--surface-3);
|
|
9547
|
+
color: var(--surface-3-text);
|
|
9548
|
+
border: var(--border-subtle);
|
|
9549
|
+
border-radius: var(--radius-m);
|
|
9550
|
+
font-size: var(--text-s);
|
|
9551
|
+
flex-shrink: 0;
|
|
9552
|
+
}
|
|
9553
|
+
|
|
9554
|
+
.breadcrumb ixfx-crumb-navigation {
|
|
9555
|
+
flex: 1;
|
|
9556
|
+
min-width: 0;
|
|
9557
|
+
}
|
|
9558
|
+
|
|
9559
|
+
.band {
|
|
9560
|
+
position: relative;
|
|
9561
|
+
}
|
|
9562
|
+
|
|
9563
|
+
.icicle.horizontal .band {
|
|
9564
|
+
height: var(--icicletree-band-height);
|
|
9565
|
+
}
|
|
9566
|
+
|
|
9567
|
+
.icicle.vertical .band {
|
|
9568
|
+
flex: 1 1 0;
|
|
9569
|
+
min-width: 0;
|
|
9570
|
+
height: 100%;
|
|
9571
|
+
}
|
|
9572
|
+
|
|
9573
|
+
.rect {
|
|
9574
|
+
box-sizing: border-box;
|
|
9575
|
+
position: absolute;
|
|
9576
|
+
overflow: hidden;
|
|
9577
|
+
display: flex;
|
|
9578
|
+
align-items: center;
|
|
9579
|
+
padding: 0 var(--space-s);
|
|
9580
|
+
background: var(--icicletree-rect-bg);
|
|
9581
|
+
color: var(--icicletree-rect-text);
|
|
9582
|
+
font-size: var(--icicletree-font-size);
|
|
9583
|
+
cursor: pointer;
|
|
9584
|
+
transition:
|
|
9585
|
+
width var(--transition),
|
|
9586
|
+
height var(--transition),
|
|
9587
|
+
left var(--transition),
|
|
9588
|
+
top var(--transition),
|
|
9589
|
+
opacity var(--transition),
|
|
9590
|
+
background-color var(--transition);
|
|
9591
|
+
}
|
|
9592
|
+
|
|
9593
|
+
.icicle.horizontal .rect {
|
|
9594
|
+
top: 0;
|
|
9595
|
+
height: 100%;
|
|
9596
|
+
}
|
|
9597
|
+
|
|
9598
|
+
/*
|
|
9599
|
+
* A rotated label needs its full allotted width to be usable — the
|
|
9600
|
+
* default left/right padding (--space-s, ~4px a side) can consume
|
|
9601
|
+
* most or all of a rect sized down to minVerticalLabelWidth, leaving
|
|
9602
|
+
* the rotated text clipped to invisible. #labelMode's width estimate
|
|
9603
|
+
* assumes the whole rect width is usable, so drop the padding here to
|
|
9604
|
+
* match that assumption instead of also budgeting for it in the
|
|
9605
|
+
* threshold (which would make already-narrow rects need even more).
|
|
9606
|
+
*/
|
|
9607
|
+
.icicle.horizontal .rect.rotated {
|
|
9608
|
+
padding: 0;
|
|
9609
|
+
}
|
|
9610
|
+
|
|
9611
|
+
.icicle.vertical .rect {
|
|
9612
|
+
left: 0;
|
|
9613
|
+
width: 100%;
|
|
9614
|
+
padding: var(--space-s) 0;
|
|
9615
|
+
justify-content: center;
|
|
9616
|
+
writing-mode: horizontal-tb;
|
|
9617
|
+
}
|
|
9618
|
+
|
|
9619
|
+
.rect.filtered {
|
|
9620
|
+
opacity: 0.3;
|
|
9621
|
+
filter: saturate(0.4);
|
|
9622
|
+
}
|
|
9623
|
+
|
|
9624
|
+
.rect.empty {
|
|
9625
|
+
padding: 0;
|
|
9626
|
+
border: none;
|
|
9627
|
+
}
|
|
9628
|
+
|
|
9629
|
+
.rect.loading {
|
|
9630
|
+
cursor: progress;
|
|
9631
|
+
opacity: 0.6;
|
|
9632
|
+
}
|
|
9633
|
+
|
|
9634
|
+
.icicle-label {
|
|
9635
|
+
overflow: hidden;
|
|
9636
|
+
text-overflow: ellipsis;
|
|
9637
|
+
white-space: nowrap;
|
|
9638
|
+
flex: 1;
|
|
9639
|
+
min-width: 0;
|
|
9640
|
+
}
|
|
9641
|
+
|
|
9642
|
+
.icicle-label.hide-label {
|
|
9643
|
+
visibility: hidden;
|
|
9644
|
+
}
|
|
9645
|
+
|
|
9646
|
+
.icicle-label.label-vertical {
|
|
9647
|
+
writing-mode: vertical-rl;
|
|
9648
|
+
text-orientation: sideways;
|
|
9649
|
+
flex: none;
|
|
9650
|
+
width: 100%;
|
|
9651
|
+
height: 100%;
|
|
9652
|
+
padding: var(--space-2xs, 2px) 0;
|
|
9653
|
+
box-sizing: border-box;
|
|
9654
|
+
display: flex;
|
|
9655
|
+
align-items: center;
|
|
9656
|
+
justify-content: center;
|
|
9657
|
+
}
|
|
9658
|
+
|
|
9659
|
+
/*
|
|
9660
|
+
* Instant tooltip for rects whose label is rotated or hidden — anchored
|
|
9661
|
+
* via CSS anchor positioning (anchor-name set on the tickled rect in
|
|
9662
|
+
* #renderRect) rather than a JS-computed position, and shown/hidden via
|
|
9663
|
+
* the Popover API rather than the browser's own (slow, un-stylable)
|
|
9664
|
+
* title tooltip.
|
|
9665
|
+
*/
|
|
9666
|
+
.icicle-tooltip {
|
|
9667
|
+
position: fixed;
|
|
9668
|
+
position-anchor: --icicle-rect-anchor;
|
|
9669
|
+
position-area: bottom center;
|
|
9670
|
+
position-try-fallbacks: top center;
|
|
9671
|
+
margin: var(--space-xs, 4px) 0 0;
|
|
9672
|
+
padding: var(--space-2xs, 2px) var(--space-xs, 6px);
|
|
9673
|
+
background: var(--surface-5);
|
|
9674
|
+
color: var(--surface-5-text);
|
|
9675
|
+
border: var(--border-subtle);
|
|
9676
|
+
border-radius: var(--radius-s, 4px);
|
|
9677
|
+
font-size: var(--text-s);
|
|
9678
|
+
white-space: nowrap;
|
|
9679
|
+
pointer-events: none;
|
|
9680
|
+
inset: auto;
|
|
9681
|
+
}
|
|
9682
|
+
|
|
9683
|
+
.icicle-tooltip:popover-open {
|
|
9684
|
+
display: block;
|
|
9685
|
+
}
|
|
9686
|
+
|
|
9687
|
+
.empty-state {
|
|
9688
|
+
padding: var(--space-l);
|
|
9689
|
+
color: var(--surface-muted-text);
|
|
9690
|
+
font-size: var(--text-s);
|
|
9691
|
+
}
|
|
9692
|
+
`;
|
|
9693
|
+
//#endregion
|
|
8881
9694
|
//#region src/icicle-tree/icicletree-list.ts
|
|
8882
9695
|
let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
8883
9696
|
constructor(..._args) {
|
|
@@ -8895,6 +9708,14 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8895
9708
|
this.disableKeyboardNavigation = false;
|
|
8896
9709
|
this.registry = void 0;
|
|
8897
9710
|
this._zoomKey = void 0;
|
|
9711
|
+
this.restoreLoads = true;
|
|
9712
|
+
this.#viewState = new ViewStateController(this, {
|
|
9713
|
+
kind: `icicletree-list`,
|
|
9714
|
+
capture: () => this.getViewState()
|
|
9715
|
+
});
|
|
9716
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9717
|
+
this.#keyboardCursor = false;
|
|
9718
|
+
this.#zoomPath = [];
|
|
8898
9719
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
8899
9720
|
labelSelector: `.icicle-label`,
|
|
8900
9721
|
highlightKey: `incr-search-highlight`
|
|
@@ -8923,6 +9744,9 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8923
9744
|
});
|
|
8924
9745
|
this.#pendingKeys = /* @__PURE__ */ new Set();
|
|
8925
9746
|
this.#aborters = /* @__PURE__ */ new Map();
|
|
9747
|
+
this.#onScroll = () => {
|
|
9748
|
+
if (!this.#viewState.pendingScroll) this.#viewState.notify();
|
|
9749
|
+
};
|
|
8926
9750
|
}
|
|
8927
9751
|
static {
|
|
8928
9752
|
this.shadowRootOptions = {
|
|
@@ -8938,6 +9762,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8938
9762
|
if (this._zoomKey === void 0) return void 0;
|
|
8939
9763
|
return findIcicleNode(this.#effectiveRoot, this._zoomKey);
|
|
8940
9764
|
}
|
|
9765
|
+
#viewState;
|
|
9766
|
+
/** Saved selection for keys not (yet) loaded. */
|
|
9767
|
+
#pendingSelected;
|
|
9768
|
+
/** Saved zoom path not (yet) resolvable. */
|
|
9769
|
+
#pendingZoom;
|
|
9770
|
+
#pendingFocus;
|
|
9771
|
+
/** Whether the cursor was last placed by the keyboard (only then is it `focus`). */
|
|
9772
|
+
#keyboardCursor;
|
|
9773
|
+
/** Last known key path of the zoom node, so it can go pending when the node leaves. */
|
|
9774
|
+
#zoomPath;
|
|
8941
9775
|
#modelUnsubscribe;
|
|
8942
9776
|
#pendingKeys;
|
|
8943
9777
|
#aborters;
|
|
@@ -8974,6 +9808,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8974
9808
|
});
|
|
8975
9809
|
this.registry = registry;
|
|
8976
9810
|
this.#syncSearchCommands();
|
|
9811
|
+
this.#viewState.observeScrollTarget(this);
|
|
9812
|
+
this.addEventListener(`scroll`, this.#onScroll, { passive: true });
|
|
8977
9813
|
if (typeof ResizeObserver !== `undefined`) {
|
|
8978
9814
|
this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
|
|
8979
9815
|
this.#resizeObserver.observe(this);
|
|
@@ -8986,6 +9822,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
8986
9822
|
for (const aborter of this.#aborters.values()) aborter.abort();
|
|
8987
9823
|
this.#aborters.clear();
|
|
8988
9824
|
this.#pendingKeys.clear();
|
|
9825
|
+
this.removeEventListener(`scroll`, this.#onScroll);
|
|
8989
9826
|
this.#resizeObserver?.disconnect();
|
|
8990
9827
|
this.#resizeObserver = void 0;
|
|
8991
9828
|
if (this.#ownedRegistry) {
|
|
@@ -9007,28 +9844,23 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9007
9844
|
this.#unregisterSearchCommands?.();
|
|
9008
9845
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
9009
9846
|
}
|
|
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`)) {
|
|
9847
|
+
willUpdate(changedProps) {
|
|
9848
|
+
if (changedProps.has(`root`) || changedProps.has(`model`)) this.#anchorIndex = void 0;
|
|
9849
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
|
|
9026
9850
|
this.#anchorIndex = void 0;
|
|
9027
9851
|
this.clearSelection();
|
|
9028
9852
|
}
|
|
9853
|
+
this.#reconcileViewState();
|
|
9854
|
+
}
|
|
9855
|
+
updated(changedProps) {
|
|
9856
|
+
if (changedProps.has(`model`)) {
|
|
9857
|
+
this.#modelUnsubscribe?.();
|
|
9858
|
+
this.#modelUnsubscribe = this.model?._subscribe(() => this.requestUpdate());
|
|
9859
|
+
}
|
|
9029
9860
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
9030
|
-
if (this._zoomKey !== void 0 && !findIcicleNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
9031
9861
|
this.#ensureVisibleChildren();
|
|
9862
|
+
this.#continueRestoreLoads();
|
|
9863
|
+
this.#viewState.applyPendingScroll((scroll) => applyHostScroll(this, scroll));
|
|
9032
9864
|
this.#syncTooltip();
|
|
9033
9865
|
}
|
|
9034
9866
|
/** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
|
|
@@ -9078,12 +9910,103 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9078
9910
|
popover.hidePopover();
|
|
9079
9911
|
} catch {}
|
|
9080
9912
|
}
|
|
9081
|
-
|
|
9082
|
-
|
|
9083
|
-
|
|
9084
|
-
|
|
9085
|
-
|
|
9913
|
+
/**
|
|
9914
|
+
* Snapshot selection, zoom path, keyboard cursor and scroll as plain JSON,
|
|
9915
|
+
* keyed by `item.key`. Includes pending state for keys not currently loaded.
|
|
9916
|
+
*/
|
|
9917
|
+
getViewState(options) {
|
|
9918
|
+
const withPending = options?.includePending ?? true;
|
|
9919
|
+
const focus = this.#keyboardCursor ? this.tickled.tickledId : void 0;
|
|
9920
|
+
return this.#viewState.shape({
|
|
9921
|
+
selected: keysToArray([...this._selectedNodes].map((n) => n.item.key), withPending ? this.#pendingSelected : []),
|
|
9922
|
+
zoom: (withPending ? this.#pendingZoom : void 0) ?? (this._zoomKey === void 0 ? [] : [...this.#zoomPath]),
|
|
9923
|
+
focus: focus ?? (withPending ? this.#pendingFocus : void 0),
|
|
9924
|
+
scroll: (withPending ? this.#viewState.pendingScroll : void 0) ?? (this.isConnected ? captureHostScroll(this) : void 0)
|
|
9925
|
+
}, options);
|
|
9926
|
+
}
|
|
9927
|
+
/**
|
|
9928
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
9929
|
+
* that aren't loaded yet are applied as they appear — including via
|
|
9930
|
+
* `loadChildren` / `model` down a saved zoom path when `restoreLoads`.
|
|
9931
|
+
*/
|
|
9932
|
+
setViewState(state) {
|
|
9933
|
+
if (!this.#viewState.accepts(state)) return;
|
|
9934
|
+
this.#viewState.restoring(() => {
|
|
9935
|
+
if (state.selected) {
|
|
9936
|
+
this.#pendingSelected = capKeys(new Set(state.selected));
|
|
9937
|
+
if (this._selectedNodes.size > 0) {
|
|
9938
|
+
const prev = new Set(this._selectedNodes);
|
|
9939
|
+
this.#clearSelectionInternal();
|
|
9940
|
+
this.#dispatchSelect(prev);
|
|
9941
|
+
}
|
|
9942
|
+
}
|
|
9943
|
+
if (state.zoom) this.#pendingZoom = [...state.zoom];
|
|
9944
|
+
if (state.focus !== void 0) this.#pendingFocus = state.focus;
|
|
9945
|
+
if (state.scroll) this.#viewState.pendingScroll = state.scroll;
|
|
9946
|
+
this.#reconcileViewState();
|
|
9947
|
+
if (this.#pendingZoom !== void 0) this.zoomOut();
|
|
9948
|
+
});
|
|
9949
|
+
this.requestUpdate();
|
|
9950
|
+
}
|
|
9951
|
+
clearPendingViewState() {
|
|
9952
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9953
|
+
this.#pendingZoom = void 0;
|
|
9954
|
+
this.#pendingFocus = void 0;
|
|
9955
|
+
this.#viewState.pendingScroll = void 0;
|
|
9956
|
+
}
|
|
9957
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
9958
|
+
#reconcileViewState() {
|
|
9959
|
+
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;
|
|
9960
|
+
const root = this.#effectiveRoot;
|
|
9961
|
+
let index;
|
|
9962
|
+
const nodeByKey = (key) => (index ??= indexNodesByKey(root)).get(key);
|
|
9963
|
+
this.#viewState.restoring(() => {
|
|
9964
|
+
this.#reconcileZoom(root, nodeByKey);
|
|
9965
|
+
const sel = reconcileSelection$1(this._selectedNodes, this.#pendingSelected, nodeByKey, (node) => this.#canSelect(node), this.interactionMode === `implicit`);
|
|
9966
|
+
if (sel.changed) {
|
|
9967
|
+
const prev = this._selectedNodes;
|
|
9968
|
+
const primary = this.selectedNode?.item.key;
|
|
9969
|
+
this._selectedNodes = new Set(sel.selected);
|
|
9970
|
+
this.#pendingSelected = sel.pending;
|
|
9971
|
+
this.selectedNode = [...sel.selected].find((n) => n.item.key === primary) ?? [...sel.selected].at(-1);
|
|
9972
|
+
if (sel.restored) this.#dispatchSelect(prev);
|
|
9973
|
+
else this.#viewState.notify(`data`);
|
|
9974
|
+
}
|
|
9975
|
+
this.#reconcileFocus(nodeByKey);
|
|
9976
|
+
});
|
|
9977
|
+
}
|
|
9978
|
+
#reconcileZoom(root, nodeByKey) {
|
|
9979
|
+
if (this._zoomKey !== void 0 && !nodeByKey(this._zoomKey)) {
|
|
9980
|
+
this.#pendingZoom ??= this.#zoomPath;
|
|
9981
|
+
this._zoomKey = void 0;
|
|
9982
|
+
this.#viewState.notify(`data`);
|
|
9983
|
+
}
|
|
9984
|
+
const nodes = this.#pendingZoom && resolveZoomPath$1(root, this.#pendingZoom, nodeByKey);
|
|
9985
|
+
if (!nodes) return;
|
|
9986
|
+
this.#pendingZoom = void 0;
|
|
9987
|
+
if (nodes.length === 0) this.zoomOut();
|
|
9988
|
+
else this.zoomTo(nodes.at(-1));
|
|
9989
|
+
}
|
|
9990
|
+
#reconcileFocus(nodeByKey) {
|
|
9991
|
+
const tickled = this.tickled.tickledId;
|
|
9992
|
+
if (tickled !== void 0 && !nodeByKey(tickled)) {
|
|
9993
|
+
if (this.#keyboardCursor) this.#pendingFocus ??= tickled;
|
|
9994
|
+
this.tickled.clearTickled();
|
|
9995
|
+
}
|
|
9996
|
+
const focus = this.#pendingFocus;
|
|
9997
|
+
if (focus !== void 0 && this.#rects().some((r) => r.node.item.key === focus)) {
|
|
9998
|
+
this.#pendingFocus = void 0;
|
|
9999
|
+
this.#keyboardCursor = true;
|
|
10000
|
+
this.tickled.setTickled(focus);
|
|
10001
|
+
}
|
|
9086
10002
|
}
|
|
10003
|
+
/** Load the next unloaded node along a pending zoom path (one load at a time). */
|
|
10004
|
+
#continueRestoreLoads() {
|
|
10005
|
+
if (!this.restoreLoads || this.#pendingZoom === void 0 || !this.model && !this.loadChildren) return;
|
|
10006
|
+
const next = resolveKeyPath(this.#effectiveRoot, this.#pendingZoom, (node) => node.item.isLeaf !== true);
|
|
10007
|
+
if (next.status === `load` && !this.#pendingKeys.has(next.node.item.key)) this.#loadChildren(next.node, next.depth);
|
|
10008
|
+
}
|
|
10009
|
+
#onScroll;
|
|
9087
10010
|
#ensureVisibleChildren() {
|
|
9088
10011
|
if (!this.model && !this.loadChildren) return;
|
|
9089
10012
|
for (const rect of this.#rects()) {
|
|
@@ -9127,6 +10050,8 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9127
10050
|
this.selectedNode = void 0;
|
|
9128
10051
|
}
|
|
9129
10052
|
#dispatchSelect(previous) {
|
|
10053
|
+
if (!this.#viewState.isRestoring) this.#pendingSelected = EMPTY_KEYS;
|
|
10054
|
+
this.#viewState.notify();
|
|
9130
10055
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
9131
10056
|
detail: {
|
|
9132
10057
|
selected: new Set(this._selectedNodes),
|
|
@@ -9154,6 +10079,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9154
10079
|
this.requestUpdate();
|
|
9155
10080
|
}
|
|
9156
10081
|
clearSelection() {
|
|
10082
|
+
this.#pendingSelected = EMPTY_KEYS;
|
|
9157
10083
|
if (this._selectedNodes.size === 0) return;
|
|
9158
10084
|
const prev = new Set(this._selectedNodes);
|
|
9159
10085
|
this.#clearSelectionInternal();
|
|
@@ -9190,11 +10116,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9190
10116
|
* Fires `zoom` and the semantic `expand` event.
|
|
9191
10117
|
*/
|
|
9192
10118
|
zoomTo(node) {
|
|
10119
|
+
this.#zoomChanged();
|
|
9193
10120
|
if (this._zoomKey === node.item.key) return;
|
|
9194
10121
|
this._zoomKey = node.item.key;
|
|
9195
10122
|
this.tickled.clearTickled();
|
|
9196
|
-
const
|
|
9197
|
-
|
|
10123
|
+
const path = findIciclePath(this.#effectiveRoot, node.item.key);
|
|
10124
|
+
this.#zoomPath = path?.slice(1).map((n) => n.item.key) ?? [node.item.key];
|
|
10125
|
+
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
9198
10126
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9199
10127
|
detail: {
|
|
9200
10128
|
node,
|
|
@@ -9218,11 +10146,13 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9218
10146
|
* the semantic `collapse` event for the previous zoom root.
|
|
9219
10147
|
*/
|
|
9220
10148
|
zoomOut() {
|
|
10149
|
+
this.#zoomChanged();
|
|
9221
10150
|
if (this._zoomKey === void 0) return;
|
|
9222
10151
|
const previous = this.focusedNode;
|
|
9223
10152
|
const path = this._zoomKey === void 0 ? void 0 : findIciclePath(this.#effectiveRoot, this._zoomKey);
|
|
9224
10153
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
9225
10154
|
this._zoomKey = void 0;
|
|
10155
|
+
this.#zoomPath = [];
|
|
9226
10156
|
this.tickled.clearTickled();
|
|
9227
10157
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
9228
10158
|
detail: {
|
|
@@ -9242,6 +10172,11 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9242
10172
|
}));
|
|
9243
10173
|
this.requestUpdate();
|
|
9244
10174
|
}
|
|
10175
|
+
/** An explicit zoom supersedes a saved one still waiting. */
|
|
10176
|
+
#zoomChanged() {
|
|
10177
|
+
if (!this.#viewState.isRestoring) this.#pendingZoom = void 0;
|
|
10178
|
+
this.#viewState.notify();
|
|
10179
|
+
}
|
|
9245
10180
|
#canZoom(node) {
|
|
9246
10181
|
if (node.item.isLeaf === true) return false;
|
|
9247
10182
|
if (node.children !== void 0) return node.children.length > 0;
|
|
@@ -9277,7 +10212,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9277
10212
|
navigateToNode(predicate) {
|
|
9278
10213
|
const root = this.#effectiveRoot;
|
|
9279
10214
|
if (!root?.children) return false;
|
|
9280
|
-
const keyPath =
|
|
10215
|
+
const keyPath = findIcicleKeyPath(root.children, predicate);
|
|
9281
10216
|
if (!keyPath) return false;
|
|
9282
10217
|
return this.navigateTo(keyPath);
|
|
9283
10218
|
}
|
|
@@ -9288,35 +10223,16 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9288
10223
|
revealKey(key) {
|
|
9289
10224
|
const root = this.#effectiveRoot;
|
|
9290
10225
|
if (!root?.children) return false;
|
|
9291
|
-
const path =
|
|
10226
|
+
const path = findIcicleKeyPath(root.children, (n) => n.item.key === key);
|
|
9292
10227
|
if (!path) return false;
|
|
9293
10228
|
if (path.length > this.maxDepth) {
|
|
9294
|
-
const ancestor = this.#
|
|
10229
|
+
const ancestor = resolveIcicleKeyPath(this.#effectiveRoot, path.slice(0, path.length - this.maxDepth));
|
|
9295
10230
|
if (ancestor) this.zoomTo(ancestor);
|
|
9296
10231
|
}
|
|
9297
10232
|
this.tickled.setTickled(key);
|
|
9298
10233
|
this.requestUpdate();
|
|
9299
10234
|
return true;
|
|
9300
10235
|
}
|
|
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
10236
|
/**
|
|
9321
10237
|
* All nodes currently visible as rectangles, in render (band-major) order,
|
|
9322
10238
|
* with absolute depth (root's children are depth 0).
|
|
@@ -9393,44 +10309,20 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9393
10309
|
const currentIndex = tickledKey === void 0 ? -1 : rects.findIndex((r) => r.node.item.key === tickledKey);
|
|
9394
10310
|
const current = currentIndex >= 0 ? rects[currentIndex] : void 0;
|
|
9395
10311
|
const moveTo = (rect) => {
|
|
9396
|
-
if (!rect) return;
|
|
9397
10312
|
event.preventDefault();
|
|
9398
10313
|
this.tickled.setTickled(rect.node.item.key);
|
|
10314
|
+
this.#keyboardCursor = true;
|
|
10315
|
+
this.#pendingFocus = void 0;
|
|
10316
|
+
this.#viewState.notify();
|
|
9399
10317
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
9400
10318
|
};
|
|
10319
|
+
if (event.key === `ArrowLeft` || event.key === `ArrowRight`) event.preventDefault();
|
|
10320
|
+
const target = icicleKeyTarget(rects, current, event.key);
|
|
10321
|
+
if (target) {
|
|
10322
|
+
moveTo(target);
|
|
10323
|
+
return;
|
|
10324
|
+
}
|
|
9401
10325
|
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
10326
|
case `Enter`:
|
|
9435
10327
|
if (!current) return;
|
|
9436
10328
|
event.preventDefault();
|
|
@@ -9480,6 +10372,10 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9480
10372
|
if (node.item.key === this.#effectiveRoot?.item.key) this.zoomOut();
|
|
9481
10373
|
else this.zoomTo(node);
|
|
9482
10374
|
}
|
|
10375
|
+
#onRectPointerEnter(key) {
|
|
10376
|
+
this.tickled.handlePointerEnter(key);
|
|
10377
|
+
if (this.tickled.tickledId === key) this.#keyboardCursor = false;
|
|
10378
|
+
}
|
|
9483
10379
|
#renderRect(rect) {
|
|
9484
10380
|
const { node } = rect;
|
|
9485
10381
|
const selected = this._selectedNodes.has(node);
|
|
@@ -9508,7 +10404,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9508
10404
|
style=${sizeStyle}
|
|
9509
10405
|
@click=${(e) => this.#handleItemClick(rect, this.#rects().findIndex((r) => r.node.item.key === node.item.key), e)}
|
|
9510
10406
|
@dblclick=${() => this.#handleItemDblClick(rect)}
|
|
9511
|
-
@pointerenter=${() => this
|
|
10407
|
+
@pointerenter=${() => this.#onRectPointerEnter(node.item.key)}
|
|
9512
10408
|
>
|
|
9513
10409
|
<span class="icicle-label ${hideLabel ? `hide-label` : ``} ${rotated ? `label-vertical` : ``}" part="label">${node.item.label}</span>
|
|
9514
10410
|
</div>
|
|
@@ -9553,185 +10449,7 @@ let IcicleTreeListElement = class IcicleTreeListElement extends LitElement {
|
|
|
9553
10449
|
tickledStyles,
|
|
9554
10450
|
tickledItemStyles,
|
|
9555
10451
|
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
|
-
`
|
|
10452
|
+
icicleTreeStyles
|
|
9735
10453
|
];
|
|
9736
10454
|
}
|
|
9737
10455
|
};
|
|
@@ -9786,6 +10504,7 @@ __decorate([property({
|
|
|
9786
10504
|
reflect: true
|
|
9787
10505
|
})], IcicleTreeListElement.prototype, "searchModes", void 0);
|
|
9788
10506
|
__decorate([state()], IcicleTreeListElement.prototype, "_zoomKey", void 0);
|
|
10507
|
+
__decorate([property({ attribute: false })], IcicleTreeListElement.prototype, "restoreLoads", void 0);
|
|
9789
10508
|
__decorate([query(`#icicle-tooltip`)], IcicleTreeListElement.prototype, "_tooltipEl", void 0);
|
|
9790
10509
|
IcicleTreeListElement = __decorate([safeCustomElement(`ixfx-icicletree-list`)], IcicleTreeListElement);
|
|
9791
10510
|
//#endregion
|
|
@@ -9811,6 +10530,136 @@ function findLeafCategory(categories, key) {
|
|
|
9811
10530
|
return flattenCategories(categories).find((c) => c.key === key);
|
|
9812
10531
|
}
|
|
9813
10532
|
//#endregion
|
|
10533
|
+
//#region src/multicol-list/multicol-view-state.ts
|
|
10534
|
+
/**
|
|
10535
|
+
* Pure helpers for `ixfx-multicol-list` view state. The element runs these
|
|
10536
|
+
* when a state is restored, when `categories` change (so saved columns whose
|
|
10537
|
+
* category wasn't known yet can appear) and when a column's items arrive (so
|
|
10538
|
+
* saved selections apply once their keys exist).
|
|
10539
|
+
*/
|
|
10540
|
+
const MULTICOL_VIEW_STATE_KIND = `multicol-list`;
|
|
10541
|
+
const isSort = (v) => v === `asc` || v === `desc`;
|
|
10542
|
+
/**
|
|
10543
|
+
* Validate saved columns (they come from storage, so may be malformed).
|
|
10544
|
+
* Entries without a string `id` / `categoryKey` are dropped, as are repeat
|
|
10545
|
+
* ids; bad optional fields fall back to defaults.
|
|
10546
|
+
*/
|
|
10547
|
+
function sanitiseColumns(raw) {
|
|
10548
|
+
if (!Array.isArray(raw)) return [];
|
|
10549
|
+
const seen = /* @__PURE__ */ new Set();
|
|
10550
|
+
const out = [];
|
|
10551
|
+
for (const entry of raw) {
|
|
10552
|
+
if (entry === null || typeof entry !== `object`) continue;
|
|
10553
|
+
const e = entry;
|
|
10554
|
+
if (typeof e.id !== `string` || typeof e.categoryKey !== `string` || seen.has(e.id)) continue;
|
|
10555
|
+
seen.add(e.id);
|
|
10556
|
+
out.push({
|
|
10557
|
+
id: e.id,
|
|
10558
|
+
categoryKey: e.categoryKey,
|
|
10559
|
+
sort: isSort(e.sort) ? e.sort : `asc`,
|
|
10560
|
+
grouped: e.grouped === true,
|
|
10561
|
+
...typeof e.menu === `boolean` ? { menu: e.menu } : {},
|
|
10562
|
+
...typeof e.width === `number` && Number.isFinite(e.width) ? { width: e.width } : {}
|
|
10563
|
+
});
|
|
10564
|
+
}
|
|
10565
|
+
return out;
|
|
10566
|
+
}
|
|
10567
|
+
/** Split saved columns into those whose category resolves now and those to keep pending. */
|
|
10568
|
+
function partitionColumns(columns, hasCategory) {
|
|
10569
|
+
const ready = [];
|
|
10570
|
+
const pending = [];
|
|
10571
|
+
columns.forEach((state, index) => {
|
|
10572
|
+
if (hasCategory(state.categoryKey)) ready.push(state);
|
|
10573
|
+
else pending.push({
|
|
10574
|
+
state,
|
|
10575
|
+
index
|
|
10576
|
+
});
|
|
10577
|
+
});
|
|
10578
|
+
return {
|
|
10579
|
+
ready,
|
|
10580
|
+
pending
|
|
10581
|
+
};
|
|
10582
|
+
}
|
|
10583
|
+
/** Split pending columns into those whose category now resolves and those still waiting. */
|
|
10584
|
+
function resolvePendingColumns(pending, hasCategory) {
|
|
10585
|
+
if (pending.length === 0) return {
|
|
10586
|
+
ready: [],
|
|
10587
|
+
pending
|
|
10588
|
+
};
|
|
10589
|
+
const ready = pending.filter((p) => hasCategory(p.state.categoryKey));
|
|
10590
|
+
return ready.length === 0 ? {
|
|
10591
|
+
ready,
|
|
10592
|
+
pending
|
|
10593
|
+
} : {
|
|
10594
|
+
ready,
|
|
10595
|
+
pending: pending.filter((p) => !hasCategory(p.state.categoryKey))
|
|
10596
|
+
};
|
|
10597
|
+
}
|
|
10598
|
+
/** Insert pending columns back at their saved positions (clamped), for a snapshot. */
|
|
10599
|
+
function mergeColumns(live, pending) {
|
|
10600
|
+
const out = [...live];
|
|
10601
|
+
for (const p of [...pending].sort((a, b) => a.index - b.index)) out.splice(Math.min(p.index, out.length), 0, p.state);
|
|
10602
|
+
return out;
|
|
10603
|
+
}
|
|
10604
|
+
/**
|
|
10605
|
+
* - selected keys no longer in the data → pending
|
|
10606
|
+
* - pending keys now in the data → selected (appended, in pending order)
|
|
10607
|
+
*/
|
|
10608
|
+
function reconcileSelection(selected, pending, has) {
|
|
10609
|
+
const kept = selected.filter(has);
|
|
10610
|
+
const dropped = kept.length === selected.length ? [] : selected.filter((k) => !has(k));
|
|
10611
|
+
const { present, missing } = partitionKeys(pending, has);
|
|
10612
|
+
if (dropped.length === 0 && present.length === 0) return {
|
|
10613
|
+
selected,
|
|
10614
|
+
pending,
|
|
10615
|
+
restored: false,
|
|
10616
|
+
changed: false
|
|
10617
|
+
};
|
|
10618
|
+
const nextPending = dropped.length === 0 ? missing : capKeys(/* @__PURE__ */ new Set([...missing, ...dropped]));
|
|
10619
|
+
const added = present.filter((k) => !kept.includes(k));
|
|
10620
|
+
return {
|
|
10621
|
+
selected: [...kept, ...added],
|
|
10622
|
+
pending: nextPending,
|
|
10623
|
+
restored: added.length > 0,
|
|
10624
|
+
changed: dropped.length > 0 || added.length > 0
|
|
10625
|
+
};
|
|
10626
|
+
}
|
|
10627
|
+
/** JSON array of string keys → capped pending set; non-strings are ignored. */
|
|
10628
|
+
function keysFromJson(raw) {
|
|
10629
|
+
if (!Array.isArray(raw)) return EMPTY_KEYS;
|
|
10630
|
+
return capKeys(new Set(raw.filter((k) => typeof k === `string`)));
|
|
10631
|
+
}
|
|
10632
|
+
/** Whether `raw` looks like a {@link ScrollState}. */
|
|
10633
|
+
function isScrollState(raw) {
|
|
10634
|
+
return raw !== null && typeof raw === `object` && typeof raw.top === `number`;
|
|
10635
|
+
}
|
|
10636
|
+
/** `record` entries as a map, keeping only values `accept` allows. */
|
|
10637
|
+
function mapFromRecord(record, accept) {
|
|
10638
|
+
const out = /* @__PURE__ */ new Map();
|
|
10639
|
+
if (record === null || typeof record !== `object`) return out;
|
|
10640
|
+
for (const [key, value] of Object.entries(record)) {
|
|
10641
|
+
const v = accept(value);
|
|
10642
|
+
if (v !== void 0) out.set(key, v);
|
|
10643
|
+
}
|
|
10644
|
+
return out;
|
|
10645
|
+
}
|
|
10646
|
+
const keyOf = (el) => el.dataset.key;
|
|
10647
|
+
/** Rendered, keyed item rows inside a column's scroll host, in visual order. */
|
|
10648
|
+
function itemRows(host) {
|
|
10649
|
+
return [...host.querySelectorAll(`li[data-key]`)];
|
|
10650
|
+
}
|
|
10651
|
+
function captureColumnScroll(host) {
|
|
10652
|
+
return captureElementScroll(host, itemRows(host), keyOf);
|
|
10653
|
+
}
|
|
10654
|
+
/** Apply `scroll` to a column's host; `true` once the position was fully reached. */
|
|
10655
|
+
function restoreColumnScroll(host, scroll) {
|
|
10656
|
+
const anchor = scroll.anchor;
|
|
10657
|
+
if (!anchor) return scrollToOffset(host, scroll.top);
|
|
10658
|
+
const row = itemRows(host).find((el) => keyOf(el) === anchor.key);
|
|
10659
|
+
if (!row) return false;
|
|
10660
|
+
return restoreElementScroll(host, row, anchor.offset);
|
|
10661
|
+
}
|
|
10662
|
+
//#endregion
|
|
9814
10663
|
//#region src/multicol-list/types.ts
|
|
9815
10664
|
const ALL_KEY = `__all__`;
|
|
9816
10665
|
//#endregion
|
|
@@ -9862,6 +10711,7 @@ var MulticolColumnController = class {
|
|
|
9862
10711
|
#dataProvider;
|
|
9863
10712
|
#onSelectionChange;
|
|
9864
10713
|
#onRequestData;
|
|
10714
|
+
#onScroll;
|
|
9865
10715
|
#abortController = void 0;
|
|
9866
10716
|
#host;
|
|
9867
10717
|
#allRow;
|
|
@@ -9869,7 +10719,19 @@ var MulticolColumnController = class {
|
|
|
9869
10719
|
#selectedValues = [];
|
|
9870
10720
|
#allSelected = true;
|
|
9871
10721
|
#lastItems = [];
|
|
10722
|
+
/** Keys of `#lastItems`; empty until data for the current category arrived. */
|
|
10723
|
+
#itemKeys = EMPTY_KEYS;
|
|
10724
|
+
/** Saved selection keys not in the data (yet). */
|
|
10725
|
+
#pendingSelection = EMPTY_KEYS;
|
|
10726
|
+
/** Saved scroll waiting for its anchor row / enough content. */
|
|
10727
|
+
#pendingScroll;
|
|
10728
|
+
#disposeScrollIntent;
|
|
9872
10729
|
#syncingSelection = false;
|
|
10730
|
+
/**
|
|
10731
|
+
* Pending `#restoreAfterSync` passes. The lister re-renders new items and
|
|
10732
|
+
* may fire `select` before the sync runs; that isn't the user's doing.
|
|
10733
|
+
*/
|
|
10734
|
+
#syncsInFlight = 0;
|
|
9873
10735
|
#state;
|
|
9874
10736
|
constructor(options) {
|
|
9875
10737
|
this.id = options.id;
|
|
@@ -9879,6 +10741,7 @@ var MulticolColumnController = class {
|
|
|
9879
10741
|
this.#dataProvider = options.dataProvider;
|
|
9880
10742
|
this.#onSelectionChange = options.onSelectionChange;
|
|
9881
10743
|
this.#onRequestData = options.onRequestData;
|
|
10744
|
+
this.#onScroll = options.onScroll;
|
|
9882
10745
|
this.#state = {
|
|
9883
10746
|
id: options.id,
|
|
9884
10747
|
categoryKey: options.category.key,
|
|
@@ -9954,7 +10817,10 @@ var MulticolColumnController = class {
|
|
|
9954
10817
|
grouped: category.defaults?.grouped ?? false,
|
|
9955
10818
|
menu: category.defaults?.menu ?? true
|
|
9956
10819
|
};
|
|
9957
|
-
this
|
|
10820
|
+
this.#pendingSelection = EMPTY_KEYS;
|
|
10821
|
+
this.#pendingScroll = void 0;
|
|
10822
|
+
this.#itemKeys = EMPTY_KEYS;
|
|
10823
|
+
this.#setAllSelection(`data`);
|
|
9958
10824
|
this.#syncGrouping();
|
|
9959
10825
|
}
|
|
9960
10826
|
get listerHost() {
|
|
@@ -9979,7 +10845,7 @@ var MulticolColumnController = class {
|
|
|
9979
10845
|
allRow.appendChild(allLabel);
|
|
9980
10846
|
const applyAll = () => {
|
|
9981
10847
|
if (this.#allSelected) return;
|
|
9982
|
-
this.#setAllSelection();
|
|
10848
|
+
this.#setAllSelection(`user`);
|
|
9983
10849
|
};
|
|
9984
10850
|
allRow.addEventListener(`click`, applyAll);
|
|
9985
10851
|
allRow.addEventListener(`keydown`, (e) => {
|
|
@@ -10000,6 +10866,10 @@ var MulticolColumnController = class {
|
|
|
10000
10866
|
this.#syncGrouping();
|
|
10001
10867
|
this.#syncAllRow();
|
|
10002
10868
|
host.append(allRow, lister);
|
|
10869
|
+
host.addEventListener(`scroll`, this.#handleScroll, { passive: true });
|
|
10870
|
+
this.#disposeScrollIntent = onUserScrollIntent(host, () => {
|
|
10871
|
+
this.#pendingScroll = void 0;
|
|
10872
|
+
});
|
|
10003
10873
|
if (this.#lastItems.length > 0) {
|
|
10004
10874
|
this.#pushItems();
|
|
10005
10875
|
this.#restoreAfterSync();
|
|
@@ -10007,6 +10877,9 @@ var MulticolColumnController = class {
|
|
|
10007
10877
|
}
|
|
10008
10878
|
unmount() {
|
|
10009
10879
|
this.#abortPending();
|
|
10880
|
+
this.#host?.removeEventListener(`scroll`, this.#handleScroll);
|
|
10881
|
+
this.#disposeScrollIntent?.();
|
|
10882
|
+
this.#disposeScrollIntent = void 0;
|
|
10010
10883
|
this.#allRow?.remove();
|
|
10011
10884
|
this.#listerEl?.remove();
|
|
10012
10885
|
this.#allRow = void 0;
|
|
@@ -10029,17 +10902,17 @@ var MulticolColumnController = class {
|
|
|
10029
10902
|
}
|
|
10030
10903
|
#applyItems(items) {
|
|
10031
10904
|
this.#lastItems = items;
|
|
10032
|
-
const
|
|
10033
|
-
|
|
10034
|
-
this.#selectedValues
|
|
10035
|
-
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10905
|
+
const keys = new Set(items.map((i) => i.key));
|
|
10906
|
+
this.#itemKeys = keys;
|
|
10907
|
+
const r = reconcileSelection(this.#selectedValues, this.#pendingSelection, (k) => keys.has(k));
|
|
10908
|
+
this.#pendingSelection = r.pending;
|
|
10909
|
+
this.#selectedValues = r.selected;
|
|
10910
|
+
this.#allSelected = r.selected.length === 0;
|
|
10911
|
+
if (this.#listerEl) {
|
|
10912
|
+
this.#pushItems();
|
|
10913
|
+
this.#restoreAfterSync();
|
|
10039
10914
|
}
|
|
10040
|
-
this.#
|
|
10041
|
-
this.#restoreAfterSync();
|
|
10042
|
-
if (changed) this.#onSelectionChange(this.id, valid);
|
|
10915
|
+
if (r.changed) this.#onSelectionChange(this.id, r.selected, r.restored ? `restore` : `data`);
|
|
10043
10916
|
}
|
|
10044
10917
|
#pushItems() {
|
|
10045
10918
|
if (!this.#listerEl) return;
|
|
@@ -10064,7 +10937,7 @@ var MulticolColumnController = class {
|
|
|
10064
10937
|
return this.#listerEl.querySelectorAll(`:scope > .ungrouped-host > *, :scope > ixfx-panel > *`);
|
|
10065
10938
|
}
|
|
10066
10939
|
#handleSelect = () => {
|
|
10067
|
-
if (this.#syncingSelection) return;
|
|
10940
|
+
if (this.#syncingSelection || this.#syncsInFlight > 0) return;
|
|
10068
10941
|
const values = [...this.#listerEl?.selectedItems ?? []].map((i) => i.key).filter((k) => k !== ALL_KEY);
|
|
10069
10942
|
if (values.length === this.#selectedValues.length && values.every((v) => this.#selectedValues.includes(v))) {
|
|
10070
10943
|
this.#syncAllRow();
|
|
@@ -10073,9 +10946,9 @@ var MulticolColumnController = class {
|
|
|
10073
10946
|
this.#selectedValues = values;
|
|
10074
10947
|
this.#allSelected = values.length === 0;
|
|
10075
10948
|
this.#syncAllRow();
|
|
10076
|
-
this.#onSelectionChange(this.id, values);
|
|
10949
|
+
this.#onSelectionChange(this.id, values, `user`);
|
|
10077
10950
|
};
|
|
10078
|
-
#setAllSelection() {
|
|
10951
|
+
#setAllSelection(origin) {
|
|
10079
10952
|
const previous = this.#selectedValues;
|
|
10080
10953
|
this.#syncingSelection = true;
|
|
10081
10954
|
try {
|
|
@@ -10086,17 +10959,64 @@ var MulticolColumnController = class {
|
|
|
10086
10959
|
this.#syncingSelection = false;
|
|
10087
10960
|
}
|
|
10088
10961
|
this.#syncAllRow();
|
|
10089
|
-
if (previous.length > 0) this.#onSelectionChange(this.id, []);
|
|
10962
|
+
if (previous.length > 0) this.#onSelectionChange(this.id, [], origin);
|
|
10090
10963
|
}
|
|
10964
|
+
/** Back to 'All' — an explicit selection change. */
|
|
10091
10965
|
clearSelection() {
|
|
10092
|
-
this.#setAllSelection();
|
|
10966
|
+
this.#setAllSelection(`user`);
|
|
10093
10967
|
}
|
|
10094
10968
|
get selectedValues() {
|
|
10095
10969
|
return this.#selectedValues;
|
|
10096
10970
|
}
|
|
10971
|
+
get pendingSelection() {
|
|
10972
|
+
return this.#pendingSelection;
|
|
10973
|
+
}
|
|
10974
|
+
/**
|
|
10975
|
+
* Replace the selection with `keys`. Keys in the current data are selected
|
|
10976
|
+
* now; the rest stay pending until items carrying them arrive.
|
|
10977
|
+
*/
|
|
10978
|
+
restoreSelection(keys) {
|
|
10979
|
+
const previous = this.#selectedValues;
|
|
10980
|
+
const r = reconcileSelection([], capKeys(keys), (k) => this.#itemKeys.has(k));
|
|
10981
|
+
this.#pendingSelection = r.pending;
|
|
10982
|
+
this.#selectedValues = r.selected;
|
|
10983
|
+
this.#allSelected = r.selected.length === 0;
|
|
10984
|
+
this.#restoreAfterSync();
|
|
10985
|
+
this.#syncAllRow();
|
|
10986
|
+
if (!(previous.length === r.selected.length && previous.every((k) => r.selected.includes(k)))) this.#onSelectionChange(this.id, r.selected, `restore`);
|
|
10987
|
+
}
|
|
10988
|
+
clearPendingSelection() {
|
|
10989
|
+
this.#pendingSelection = EMPTY_KEYS;
|
|
10990
|
+
}
|
|
10991
|
+
get pendingScroll() {
|
|
10992
|
+
return this.#pendingScroll;
|
|
10993
|
+
}
|
|
10994
|
+
/** Current scroll of the column's list; `undefined` while unmounted. */
|
|
10995
|
+
captureScroll() {
|
|
10996
|
+
return this.#host ? captureColumnScroll(this.#host) : void 0;
|
|
10997
|
+
}
|
|
10998
|
+
/** Scroll to `scroll` now if possible, else when enough items have rendered. */
|
|
10999
|
+
restoreScroll(scroll) {
|
|
11000
|
+
this.#pendingScroll = scroll;
|
|
11001
|
+
this.#tryPendingScroll();
|
|
11002
|
+
}
|
|
11003
|
+
clearPendingScroll() {
|
|
11004
|
+
this.#pendingScroll = void 0;
|
|
11005
|
+
}
|
|
11006
|
+
#tryPendingScroll() {
|
|
11007
|
+
const scroll = this.#pendingScroll;
|
|
11008
|
+
if (!scroll || !this.#host) return;
|
|
11009
|
+
if (restoreColumnScroll(this.#host, scroll)) this.#pendingScroll = void 0;
|
|
11010
|
+
}
|
|
11011
|
+
#handleScroll = () => {
|
|
11012
|
+
if (!this.#pendingScroll) this.#onScroll?.(this.id);
|
|
11013
|
+
};
|
|
10097
11014
|
#restoreAfterSync() {
|
|
10098
11015
|
if (!this.#listerEl) return;
|
|
10099
|
-
this.#
|
|
11016
|
+
this.#syncsInFlight++;
|
|
11017
|
+
this.#listerEl.updateComplete.finally(() => {
|
|
11018
|
+
this.#syncsInFlight--;
|
|
11019
|
+
}).then(() => {
|
|
10100
11020
|
if (!this.#listerEl || !this.#host) return;
|
|
10101
11021
|
this.#syncingSelection = true;
|
|
10102
11022
|
try {
|
|
@@ -10109,6 +11029,7 @@ var MulticolColumnController = class {
|
|
|
10109
11029
|
this.#syncingSelection = false;
|
|
10110
11030
|
}
|
|
10111
11031
|
this.#syncAllRow();
|
|
11032
|
+
this.#tryPendingScroll();
|
|
10112
11033
|
});
|
|
10113
11034
|
}
|
|
10114
11035
|
#syncAllRow() {
|
|
@@ -10140,12 +11061,201 @@ function renderItem(item) {
|
|
|
10140
11061
|
}
|
|
10141
11062
|
return li;
|
|
10142
11063
|
}
|
|
11064
|
+
/** Shadow styles for `ixfx-multicol-list` (theme fallbacks / handle styles are added by the element). */
|
|
11065
|
+
const multicolListStyles = css`
|
|
11066
|
+
:host {
|
|
11067
|
+
--multicol-list-bg: var(--surface-3);
|
|
11068
|
+
--multicol-list-header-bg: var(--surface-2);
|
|
11069
|
+
--multicol-list-header-text: var(--surface-3-text);
|
|
11070
|
+
--multicol-list-border: var(--border);
|
|
11071
|
+
--multicol-list-min-column-width: ${80}px;
|
|
11072
|
+
--multicol-list-max-column-width: ${480}px;
|
|
11073
|
+
--multicol-list-header-font-size: unset;
|
|
11074
|
+
|
|
11075
|
+
display: block;
|
|
11076
|
+
overflow: hidden;
|
|
11077
|
+
/* A sideways trackpad swipe over the component must never pan it
|
|
11078
|
+
(or the page behind it, or trigger history navigation) */
|
|
11079
|
+
overscroll-behavior-x: none;
|
|
11080
|
+
background: var(--multicol-list-bg);
|
|
11081
|
+
color: var(--surface-3-text);
|
|
11082
|
+
font-family: var(--font-family);
|
|
11083
|
+
}
|
|
11084
|
+
#container {
|
|
11085
|
+
display: flex;
|
|
11086
|
+
flex-direction: row;
|
|
11087
|
+
align-items: stretch;
|
|
11088
|
+
overflow: hidden;
|
|
11089
|
+
overscroll-behavior-x: none;
|
|
11090
|
+
position: relative;
|
|
11091
|
+
height: 100%;
|
|
11092
|
+
}
|
|
11093
|
+
.column {
|
|
11094
|
+
flex-shrink: 0;
|
|
11095
|
+
display: flex;
|
|
11096
|
+
flex-direction: column;
|
|
11097
|
+
border-right: var(--multicol-list-border);
|
|
11098
|
+
min-width: var(--multicol-list-min-column-width);
|
|
11099
|
+
max-width: var(--multicol-list-max-column-width);
|
|
11100
|
+
background: var(--surface-3);
|
|
11101
|
+
}
|
|
11102
|
+
.column-header {
|
|
11103
|
+
display: flex;
|
|
11104
|
+
align-items: center;
|
|
11105
|
+
gap: var(--space-xs);
|
|
11106
|
+
height: 32px;
|
|
11107
|
+
background: var(--multicol-list-header-bg);
|
|
11108
|
+
color: var(--multicol-list-header-text);
|
|
11109
|
+
border-bottom: var(--border);
|
|
11110
|
+
position: relative;
|
|
11111
|
+
user-select: none;
|
|
11112
|
+
min-width: var(--multicol-list-min-column-width);
|
|
11113
|
+
box-sizing: border-box;
|
|
11114
|
+
}
|
|
11115
|
+
.column-header[data-dragging] {
|
|
11116
|
+
opacity: 0.4;
|
|
11117
|
+
}
|
|
11118
|
+
.reorder-handle {
|
|
11119
|
+
cursor: grab;
|
|
11120
|
+
opacity: 0;
|
|
11121
|
+
display: inline-flex;
|
|
11122
|
+
align-items: center;
|
|
11123
|
+
flex-shrink: 0;
|
|
11124
|
+
padding-left: var(--space-xs);
|
|
11125
|
+
touch-action: none;
|
|
11126
|
+
transition: opacity var(--transition, 0.15s ease-out);
|
|
11127
|
+
}
|
|
11128
|
+
.column-header:hover .reorder-handle, .column-header:focus-within .reorder-handle, .column-header[data-dragging] .reorder-handle { opacity: var(--ixfx-handle-opacity, 0.5); }
|
|
11129
|
+
.reorder-handle:active {
|
|
11130
|
+
cursor: grabbing;
|
|
11131
|
+
}
|
|
11132
|
+
.reorder-handle .ixfx-handle {
|
|
11133
|
+
--ixfx-handle-length: 14px;
|
|
11134
|
+
--ixfx-handle-thickness: var(--multicol-list-reorder-handle-width, 6px);
|
|
11135
|
+
--ixfx-handle-dot-size: 3px;
|
|
11136
|
+
}
|
|
11137
|
+
.column-label {
|
|
11138
|
+
flex: 1;
|
|
11139
|
+
overflow: hidden;
|
|
11140
|
+
text-overflow: ellipsis;
|
|
11141
|
+
white-space: nowrap;
|
|
11142
|
+
padding: 0 var(--space-s);
|
|
11143
|
+
font-size: var(--multicol-list-header-font-size);
|
|
11144
|
+
font-weight: 600;
|
|
11145
|
+
background: transparent;
|
|
11146
|
+
border: none;
|
|
11147
|
+
color: inherit;
|
|
11148
|
+
cursor: pointer;
|
|
11149
|
+
text-align: left;
|
|
11150
|
+
min-width: 0;
|
|
11151
|
+
}
|
|
11152
|
+
.column-label:hover {
|
|
11153
|
+
text-decoration: underline;
|
|
11154
|
+
}
|
|
11155
|
+
.burger-btn {
|
|
11156
|
+
flex-shrink: 0;
|
|
11157
|
+
width: 28px;
|
|
11158
|
+
height: 28px;
|
|
11159
|
+
display: flex;
|
|
11160
|
+
align-items: center;
|
|
11161
|
+
justify-content: center;
|
|
11162
|
+
background: transparent;
|
|
11163
|
+
border: none;
|
|
11164
|
+
cursor: pointer;
|
|
11165
|
+
color: var(--surface-3-text);
|
|
11166
|
+
opacity: 0;
|
|
11167
|
+
transition: opacity var(--transition);
|
|
11168
|
+
border-radius: var(--radius-s);
|
|
11169
|
+
}
|
|
11170
|
+
.column:hover .burger-btn,
|
|
11171
|
+
.column-header:focus-within .burger-btn {
|
|
11172
|
+
opacity: 1;
|
|
11173
|
+
}
|
|
11174
|
+
.burger-btn:hover {
|
|
11175
|
+
background: var(--surface-4);
|
|
11176
|
+
}
|
|
11177
|
+
.resize-handle {
|
|
11178
|
+
position: absolute;
|
|
11179
|
+
right: -4px;
|
|
11180
|
+
top: 0;
|
|
11181
|
+
bottom: 0;
|
|
11182
|
+
width: 8px;
|
|
11183
|
+
cursor: col-resize;
|
|
11184
|
+
z-index: 1;
|
|
11185
|
+
display: flex;
|
|
11186
|
+
align-items: stretch;
|
|
11187
|
+
justify-content: center;
|
|
11188
|
+
touch-action: none;
|
|
11189
|
+
}
|
|
11190
|
+
.resize-handle::before {
|
|
11191
|
+
content: '';
|
|
11192
|
+
width: 2px;
|
|
11193
|
+
margin: 2px 0;
|
|
11194
|
+
border-radius: 1px;
|
|
11195
|
+
background: transparent;
|
|
11196
|
+
transition: background-color var(--transition);
|
|
11197
|
+
}
|
|
11198
|
+
.resize-handle:hover::before,
|
|
11199
|
+
.resize-handle[data-active]::before {
|
|
11200
|
+
background: var(--accent);
|
|
11201
|
+
}
|
|
11202
|
+
.lister-host {
|
|
11203
|
+
flex: 1;
|
|
11204
|
+
overflow-y: auto;
|
|
11205
|
+
overflow-x: hidden;
|
|
11206
|
+
/* Vertical scrolling only: horizontal deltas are swallowed here
|
|
11207
|
+
instead of chaining up and panning the whole component */
|
|
11208
|
+
overscroll-behavior-x: none;
|
|
11209
|
+
touch-action: pan-y;
|
|
11210
|
+
position: relative;
|
|
11211
|
+
display: flex;
|
|
11212
|
+
flex-direction: column;
|
|
11213
|
+
--list-gutter: 0;
|
|
11214
|
+
}
|
|
11215
|
+
.all-row {
|
|
11216
|
+
display: flex;
|
|
11217
|
+
align-items: center;
|
|
11218
|
+
padding: var(--space-xs) var(--space-s);
|
|
11219
|
+
cursor: pointer;
|
|
11220
|
+
user-select: none;
|
|
11221
|
+
min-height: 28px;
|
|
11222
|
+
box-sizing: border-box;
|
|
11223
|
+
outline: 1px solid transparent;
|
|
11224
|
+
background: var(--surface-3);
|
|
11225
|
+
}
|
|
11226
|
+
.all-row:hover,
|
|
11227
|
+
.all-row[data-selected] {
|
|
11228
|
+
background: var(--surface-5);
|
|
11229
|
+
}
|
|
11230
|
+
.all-row[data-selected] {
|
|
11231
|
+
outline-color: var(--accent);
|
|
11232
|
+
font-weight: 600;
|
|
11233
|
+
}
|
|
11234
|
+
ixfx-grouped-item-lister {
|
|
11235
|
+
display: block;
|
|
11236
|
+
}
|
|
11237
|
+
.drop-caret {
|
|
11238
|
+
position: absolute;
|
|
11239
|
+
top: 0;
|
|
11240
|
+
bottom: 0;
|
|
11241
|
+
width: 2px;
|
|
11242
|
+
background: var(--accent);
|
|
11243
|
+
border-radius: 1px;
|
|
11244
|
+
pointer-events: none;
|
|
11245
|
+
z-index: 10;
|
|
11246
|
+
}
|
|
11247
|
+
.item-label {
|
|
11248
|
+
flex: 1;
|
|
11249
|
+
}
|
|
11250
|
+
.count {
|
|
11251
|
+
opacity: 0.6;
|
|
11252
|
+
margin-left: var(--space-s);
|
|
11253
|
+
font-size: var(--text-s);
|
|
11254
|
+
}
|
|
11255
|
+
`;
|
|
10143
11256
|
//#endregion
|
|
10144
11257
|
//#region src/multicol-list/multicol-list.ts
|
|
10145
11258
|
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
11259
|
const DRAG_THRESHOLD = 5;
|
|
10150
11260
|
let MulticolListElement = class MulticolListElement extends LitElement {
|
|
10151
11261
|
constructor(..._args) {
|
|
@@ -10170,240 +11280,33 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10170
11280
|
ghost: null,
|
|
10171
11281
|
insert: -1
|
|
10172
11282
|
};
|
|
11283
|
+
this.#viewState = new ViewStateController(this, {
|
|
11284
|
+
kind: MULTICOL_VIEW_STATE_KIND,
|
|
11285
|
+
capture: () => this.getViewState()
|
|
11286
|
+
});
|
|
11287
|
+
this.#pendingColumns = [];
|
|
11288
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11289
|
+
this.#pendingScroll = /* @__PURE__ */ new Map();
|
|
10173
11290
|
}
|
|
10174
11291
|
static {
|
|
10175
11292
|
this.styles = [
|
|
10176
11293
|
themeFallbacks,
|
|
10177
11294
|
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
|
-
`
|
|
11295
|
+
multicolListStyles
|
|
10369
11296
|
];
|
|
10370
11297
|
}
|
|
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
|
-
}
|
|
11298
|
+
#viewState;
|
|
11299
|
+
/** Saved columns whose category isn't in `categories` (yet). */
|
|
11300
|
+
#pendingColumns;
|
|
11301
|
+
/** Saved selection / scroll for column ids that don't exist (yet). */
|
|
11302
|
+
#pendingSelected;
|
|
11303
|
+
#pendingScroll;
|
|
10402
11304
|
addColumn(categoryKey, position) {
|
|
10403
11305
|
if (this.maxColumns > 0 && this._columns.length >= this.maxColumns) return;
|
|
10404
11306
|
const category = findLeafCategory(this.categories, categoryKey);
|
|
10405
11307
|
if (!category) return;
|
|
10406
|
-
|
|
11308
|
+
let id = `col-${++this._idCounter}`;
|
|
11309
|
+
while (this._columns.some((c) => c.id === id) || this.#pendingColumns.some((p) => p.state.id === id)) id = `col-${++this._idCounter}`;
|
|
10407
11310
|
const controller = this.#createController(id, category);
|
|
10408
11311
|
if (position !== void 0 && position >= 0 && position < this._columns.length) this._columns.splice(position, 0, controller);
|
|
10409
11312
|
else this._columns.push(controller);
|
|
@@ -10431,9 +11334,15 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10431
11334
|
}));
|
|
10432
11335
|
}
|
|
10433
11336
|
clearSelection() {
|
|
11337
|
+
this.#dropPendingSelection();
|
|
10434
11338
|
for (const col of this._columns) col.clearSelection();
|
|
10435
11339
|
this.requestUpdate();
|
|
10436
11340
|
}
|
|
11341
|
+
createRenderRoot() {
|
|
11342
|
+
const root = super.createRenderRoot();
|
|
11343
|
+
root.addEventListener(VIEW_STATE_CHANGE, (e) => e.stopPropagation());
|
|
11344
|
+
return root;
|
|
11345
|
+
}
|
|
10437
11346
|
render() {
|
|
10438
11347
|
return html`
|
|
10439
11348
|
<div id="container">
|
|
@@ -10442,7 +11351,136 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10442
11351
|
</div>
|
|
10443
11352
|
`;
|
|
10444
11353
|
}
|
|
11354
|
+
/**
|
|
11355
|
+
* Snapshot the column layout (in display order) and each column's
|
|
11356
|
+
* selection and scroll, keyed by column id. Includes pending state —
|
|
11357
|
+
* saved columns whose category isn't known and keys not yet in the data.
|
|
11358
|
+
*/
|
|
11359
|
+
getViewState(options) {
|
|
11360
|
+
const withPending = options?.includePending ?? true;
|
|
11361
|
+
const selectedByColumn = {};
|
|
11362
|
+
const scrollByColumn = {};
|
|
11363
|
+
if (withPending) {
|
|
11364
|
+
for (const [id, keys] of this.#pendingSelected) selectedByColumn[id] = [...keys];
|
|
11365
|
+
for (const [id, scroll] of this.#pendingScroll) scrollByColumn[id] = scroll;
|
|
11366
|
+
}
|
|
11367
|
+
for (const col of this._columns) {
|
|
11368
|
+
const keys = keysToArray(col.selectedValues, withPending ? col.pendingSelection : []);
|
|
11369
|
+
if (keys.length > 0) selectedByColumn[col.id] = keys;
|
|
11370
|
+
const scroll = (withPending ? col.pendingScroll : void 0) ?? col.captureScroll();
|
|
11371
|
+
if (scroll) scrollByColumn[col.id] = scroll;
|
|
11372
|
+
}
|
|
11373
|
+
return this.#viewState.shape({
|
|
11374
|
+
columns: mergeColumns(this._columns.map((c) => c.getState()), withPending ? this.#pendingColumns : []),
|
|
11375
|
+
selectedByColumn,
|
|
11376
|
+
scrollByColumn
|
|
11377
|
+
}, options);
|
|
11378
|
+
}
|
|
11379
|
+
/**
|
|
11380
|
+
* Apply saved view state; fields present replace the current ones.
|
|
11381
|
+
* `columns` creates, reorders, re-points and removes columns to match
|
|
11382
|
+
* (columns whose category isn't in `categories` wait until it is).
|
|
11383
|
+
* Selection keys not in a column's data are applied when they arrive.
|
|
11384
|
+
*/
|
|
11385
|
+
setViewState(state) {
|
|
11386
|
+
if (!this.#viewState.accepts(state)) return;
|
|
11387
|
+
this.#viewState.restoring(() => {
|
|
11388
|
+
if (state.columns) this.#applyColumns(sanitiseColumns(state.columns));
|
|
11389
|
+
if (state.selectedByColumn) {
|
|
11390
|
+
const wanted = mapFromRecord(state.selectedByColumn, (v) => Array.isArray(v) ? keysFromJson(v) : void 0);
|
|
11391
|
+
for (const col of this._columns) {
|
|
11392
|
+
col.restoreSelection(wanted.get(col.id) ?? EMPTY_KEYS);
|
|
11393
|
+
wanted.delete(col.id);
|
|
11394
|
+
}
|
|
11395
|
+
this.#pendingSelected = wanted;
|
|
11396
|
+
}
|
|
11397
|
+
if (state.scrollByColumn) {
|
|
11398
|
+
const wanted = mapFromRecord(state.scrollByColumn, (v) => isScrollState(v) ? v : void 0);
|
|
11399
|
+
for (const col of this._columns) {
|
|
11400
|
+
const scroll = wanted.get(col.id);
|
|
11401
|
+
if (scroll) col.restoreScroll(scroll);
|
|
11402
|
+
else col.clearPendingScroll();
|
|
11403
|
+
wanted.delete(col.id);
|
|
11404
|
+
}
|
|
11405
|
+
this.#pendingScroll = wanted;
|
|
11406
|
+
}
|
|
11407
|
+
this.#viewState.notify();
|
|
11408
|
+
});
|
|
11409
|
+
this.requestUpdate();
|
|
11410
|
+
}
|
|
11411
|
+
clearPendingViewState() {
|
|
11412
|
+
this.#pendingColumns = [];
|
|
11413
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11414
|
+
this.#pendingScroll = /* @__PURE__ */ new Map();
|
|
11415
|
+
for (const col of this._columns) {
|
|
11416
|
+
col.clearPendingSelection();
|
|
11417
|
+
col.clearPendingScroll();
|
|
11418
|
+
}
|
|
11419
|
+
}
|
|
11420
|
+
/** Reuse live columns by id (re-pointing their category), create the rest, drop the others. */
|
|
11421
|
+
#applyColumns(saved) {
|
|
11422
|
+
const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
|
|
11423
|
+
const { ready, pending } = partitionColumns(saved, hasCategory);
|
|
11424
|
+
const existing = new Map(this._columns.map((c) => [c.id, c]));
|
|
11425
|
+
const next = ready.map((colState) => this.#columnFromState(colState, existing));
|
|
11426
|
+
for (const col of existing.values()) col.unmount();
|
|
11427
|
+
if (saved.length > 0) this._autoSeeded = true;
|
|
11428
|
+
this.#pendingColumns = pending;
|
|
11429
|
+
this._columns = next;
|
|
11430
|
+
this._updateColOffsets();
|
|
11431
|
+
this._emitColumnsChange(`add`);
|
|
11432
|
+
this._requeryAllColumns();
|
|
11433
|
+
}
|
|
11434
|
+
/** Live column for `colState`: the existing one with that id (taken from `existing`), or a new one. */
|
|
11435
|
+
#columnFromState(colState, existing) {
|
|
11436
|
+
const category = findLeafCategory(this.categories, colState.categoryKey);
|
|
11437
|
+
const live = existing.get(colState.id);
|
|
11438
|
+
existing.delete(colState.id);
|
|
11439
|
+
if (live) {
|
|
11440
|
+
if (live.categoryKey !== colState.categoryKey) live.setCategory(category);
|
|
11441
|
+
live.updateState(colState);
|
|
11442
|
+
return live;
|
|
11443
|
+
}
|
|
11444
|
+
const controller = this.#createController(colState.id, category);
|
|
11445
|
+
controller.updateState(colState);
|
|
11446
|
+
const keys = this.#pendingSelected.get(colState.id);
|
|
11447
|
+
if (keys) {
|
|
11448
|
+
controller.restoreSelection(keys);
|
|
11449
|
+
this.#pendingSelected = withoutKey(this.#pendingSelected, colState.id);
|
|
11450
|
+
}
|
|
11451
|
+
const scroll = this.#pendingScroll.get(colState.id);
|
|
11452
|
+
if (scroll) {
|
|
11453
|
+
controller.restoreScroll(scroll);
|
|
11454
|
+
this.#pendingScroll = withoutKey(this.#pendingScroll, colState.id);
|
|
11455
|
+
}
|
|
11456
|
+
return controller;
|
|
11457
|
+
}
|
|
11458
|
+
/** Insert pending columns whose category has now appeared, at their saved positions. */
|
|
11459
|
+
#resolvePendingColumns() {
|
|
11460
|
+
const hasCategory = (key) => findLeafCategory(this.categories, key) !== void 0;
|
|
11461
|
+
const { ready, pending } = resolvePendingColumns(this.#pendingColumns, hasCategory);
|
|
11462
|
+
if (ready.length === 0) return;
|
|
11463
|
+
this.#viewState.restoring(() => {
|
|
11464
|
+
this.#pendingColumns = pending;
|
|
11465
|
+
const columns = [...this._columns];
|
|
11466
|
+
const none = /* @__PURE__ */ new Map();
|
|
11467
|
+
for (const p of [...ready].sort((a, b) => a.index - b.index)) {
|
|
11468
|
+
if (columns.some((c) => c.id === p.state.id)) continue;
|
|
11469
|
+
columns.splice(Math.min(p.index, columns.length), 0, this.#columnFromState(p.state, none));
|
|
11470
|
+
}
|
|
11471
|
+
this._columns = columns;
|
|
11472
|
+
this._updateColOffsets();
|
|
11473
|
+
this._emitColumnsChange(`add`);
|
|
11474
|
+
this._requeryAllColumns();
|
|
11475
|
+
});
|
|
11476
|
+
}
|
|
11477
|
+
/** An explicit selection change supersedes any saved selection still waiting. */
|
|
11478
|
+
#dropPendingSelection() {
|
|
11479
|
+
this.#pendingSelected = /* @__PURE__ */ new Map();
|
|
11480
|
+
for (const col of this._columns) col.clearPendingSelection();
|
|
11481
|
+
}
|
|
10445
11482
|
updated(changedProperties) {
|
|
11483
|
+
if (changedProperties.has(`categories`)) this.#resolvePendingColumns();
|
|
10446
11484
|
const seedCategory = flattenCategories(this.categories)[0];
|
|
10447
11485
|
if (!this._autoSeeded && this._columns.length === 0 && seedCategory) {
|
|
10448
11486
|
this._autoSeeded = true;
|
|
@@ -10459,8 +11497,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10459
11497
|
this._updateColOffsets();
|
|
10460
11498
|
}
|
|
10461
11499
|
_renderColumn(col, idx) {
|
|
10462
|
-
const rawWidth = col.getState().width ??
|
|
10463
|
-
const width = Number.isFinite(rawWidth) ? rawWidth :
|
|
11500
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11501
|
+
const width = Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10464
11502
|
return html`
|
|
10465
11503
|
<div
|
|
10466
11504
|
class="column"
|
|
@@ -10522,7 +11560,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10522
11560
|
selectionMode: this.selectionMode,
|
|
10523
11561
|
dataProvider: this.dataProvider,
|
|
10524
11562
|
onSelectionChange: this._onColumnSelectionChange.bind(this),
|
|
10525
|
-
onRequestData: this._onColumnRequestData.bind(this)
|
|
11563
|
+
onRequestData: this._onColumnRequestData.bind(this),
|
|
11564
|
+
onScroll: () => this.#viewState.notify()
|
|
10526
11565
|
});
|
|
10527
11566
|
}
|
|
10528
11567
|
_onHeaderPointerDown(e, col, idx) {
|
|
@@ -10601,11 +11640,11 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10601
11640
|
e.preventDefault();
|
|
10602
11641
|
e.stopPropagation();
|
|
10603
11642
|
const startX = e.clientX;
|
|
10604
|
-
const rawStartW = col.getState().width ??
|
|
10605
|
-
const startW = Number.isFinite(rawStartW) ? rawStartW :
|
|
11643
|
+
const rawStartW = col.getState().width ?? 220;
|
|
11644
|
+
const startW = Number.isFinite(rawStartW) ? rawStartW : 220;
|
|
10606
11645
|
this._resizingColumnId = col.id;
|
|
10607
11646
|
const applyWidth = (clientX) => {
|
|
10608
|
-
const w = Math.max(
|
|
11647
|
+
const w = Math.max(80, Math.min(480, startW + clientX - startX));
|
|
10609
11648
|
col.setWidth(w);
|
|
10610
11649
|
return w;
|
|
10611
11650
|
};
|
|
@@ -10734,8 +11773,10 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10734
11773
|
this._requeryAllColumns();
|
|
10735
11774
|
this.requestUpdate();
|
|
10736
11775
|
}
|
|
10737
|
-
_onColumnSelectionChange(columnId, _values) {
|
|
11776
|
+
_onColumnSelectionChange(columnId, _values, origin) {
|
|
10738
11777
|
if (!this._columns.find((c) => c.id === columnId)) return;
|
|
11778
|
+
if (origin === `user`) this.#dropPendingSelection();
|
|
11779
|
+
this.#viewState.notify(origin);
|
|
10739
11780
|
for (const otherCol of this._columns) if (otherCol.id !== columnId) otherCol.requestData(this._buildQuery(otherCol));
|
|
10740
11781
|
const detail = {
|
|
10741
11782
|
columnId,
|
|
@@ -10770,22 +11811,23 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10770
11811
|
col.setCategory(leaves[0]);
|
|
10771
11812
|
changed = true;
|
|
10772
11813
|
}
|
|
10773
|
-
if (changed) this._emitColumnsChange(`category`);
|
|
11814
|
+
if (changed) this._emitColumnsChange(`category`, `data`);
|
|
10774
11815
|
}
|
|
10775
11816
|
_updateColOffsets() {
|
|
10776
11817
|
const offsets = [0];
|
|
10777
11818
|
let sum = 0;
|
|
10778
11819
|
for (const col of this._columns) {
|
|
10779
|
-
const rawWidth = col.getState().width ??
|
|
10780
|
-
sum += Number.isFinite(rawWidth) ? rawWidth :
|
|
11820
|
+
const rawWidth = col.getState().width ?? 220;
|
|
11821
|
+
sum += Number.isFinite(rawWidth) ? rawWidth : 220;
|
|
10781
11822
|
offsets.push(sum);
|
|
10782
11823
|
}
|
|
10783
11824
|
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
11825
|
}
|
|
10785
|
-
_emitColumnsChange(reason) {
|
|
11826
|
+
_emitColumnsChange(reason, viewStateReason = `user`) {
|
|
11827
|
+
this.#viewState.notify(viewStateReason);
|
|
10786
11828
|
this.dispatchEvent(new CustomEvent(`multicol-columns-change`, {
|
|
10787
11829
|
detail: {
|
|
10788
|
-
columns: this.
|
|
11830
|
+
columns: this._columns.map((c) => c.getState()),
|
|
10789
11831
|
reason
|
|
10790
11832
|
},
|
|
10791
11833
|
bubbles: true,
|
|
@@ -10795,8 +11837,8 @@ let MulticolListElement = class MulticolListElement extends LitElement {
|
|
|
10795
11837
|
async autoSizeColumns(options) {
|
|
10796
11838
|
return new Promise((resolve) => {
|
|
10797
11839
|
setTimeout(() => {
|
|
10798
|
-
const minWidth = options?.minWidth ??
|
|
10799
|
-
const maxWidth = options?.maxWidth ??
|
|
11840
|
+
const minWidth = options?.minWidth ?? 80;
|
|
11841
|
+
const maxWidth = options?.maxWidth ?? 480;
|
|
10800
11842
|
const measure = (text) => {
|
|
10801
11843
|
const span = document.createElement(`span`);
|
|
10802
11844
|
span.style.position = `absolute`;
|
|
@@ -10874,6 +11916,11 @@ __decorate([state()], MulticolListElement.prototype, "_draggingColumnId", void 0
|
|
|
10874
11916
|
__decorate([state()], MulticolListElement.prototype, "_dropCaretX", void 0);
|
|
10875
11917
|
__decorate([query(`#container`)], MulticolListElement.prototype, "_containerEl", void 0);
|
|
10876
11918
|
MulticolListElement = __decorate([safeCustomElement(`ixfx-multicol-list`)], MulticolListElement);
|
|
11919
|
+
function withoutKey(map, key) {
|
|
11920
|
+
const next = new Map(map);
|
|
11921
|
+
next.delete(key);
|
|
11922
|
+
return next;
|
|
11923
|
+
}
|
|
10877
11924
|
//#endregion
|
|
10878
11925
|
//#region src/notifier/pill-renderer.ts
|
|
10879
11926
|
const ROW_SPACING = 8;
|
|
@@ -14536,6 +15583,242 @@ function computeTreemapLayout(root, options = {}) {
|
|
|
14536
15583
|
return rects;
|
|
14537
15584
|
}
|
|
14538
15585
|
//#endregion
|
|
15586
|
+
//#region src/treemap/treemap-view-state.ts
|
|
15587
|
+
function indexTreemapKeys(root) {
|
|
15588
|
+
const out = /* @__PURE__ */ new Map();
|
|
15589
|
+
if (!root) return out;
|
|
15590
|
+
const walk = (node, parentKey, depth) => {
|
|
15591
|
+
if (!out.has(node.item.key)) out.set(node.item.key, {
|
|
15592
|
+
node,
|
|
15593
|
+
parentKey,
|
|
15594
|
+
depth
|
|
15595
|
+
});
|
|
15596
|
+
for (const child of node.children ?? []) walk(child, node.item.key, depth + 1);
|
|
15597
|
+
};
|
|
15598
|
+
walk(root, void 0, -1);
|
|
15599
|
+
return out;
|
|
15600
|
+
}
|
|
15601
|
+
/** Key path from the root's children down to `key` (`[]` for the root itself). */
|
|
15602
|
+
function treemapKeyPath(index, key) {
|
|
15603
|
+
const path = [];
|
|
15604
|
+
for (let entry = index.get(key); entry !== void 0; entry = entry.parentKey === void 0 ? void 0 : index.get(entry.parentKey)) {
|
|
15605
|
+
if (entry.parentKey === void 0) return path;
|
|
15606
|
+
path.unshift(entry.node.item.key);
|
|
15607
|
+
}
|
|
15608
|
+
}
|
|
15609
|
+
/**
|
|
15610
|
+
* Where a saved zoom path stands against the loaded tree. The target is
|
|
15611
|
+
* matched by key anywhere (it may have moved); failing that, the deepest
|
|
15612
|
+
* loaded ancestor on the path is reported for loading when its children
|
|
15613
|
+
* haven't been fetched.
|
|
15614
|
+
*/
|
|
15615
|
+
function resolveZoomPath(index, path) {
|
|
15616
|
+
const target = path.at(-1);
|
|
15617
|
+
if (target === void 0) return {
|
|
15618
|
+
kind: `found`,
|
|
15619
|
+
key: void 0
|
|
15620
|
+
};
|
|
15621
|
+
if (index.has(target)) return {
|
|
15622
|
+
kind: `found`,
|
|
15623
|
+
key: target
|
|
15624
|
+
};
|
|
15625
|
+
for (let i = path.length - 2; i >= 0; i--) {
|
|
15626
|
+
const entry = index.get(path[i]);
|
|
15627
|
+
if (!entry) continue;
|
|
15628
|
+
const { node, depth } = entry;
|
|
15629
|
+
return node.children === void 0 && node.item.isLeaf !== true ? {
|
|
15630
|
+
kind: `load`,
|
|
15631
|
+
node,
|
|
15632
|
+
depth
|
|
15633
|
+
} : { kind: `missing` };
|
|
15634
|
+
}
|
|
15635
|
+
return { kind: `missing` };
|
|
15636
|
+
}
|
|
15637
|
+
/**
|
|
15638
|
+
* Zoom: a live zoom root that left the tree goes pending (view falls back to
|
|
15639
|
+
* the full tree); a pending path whose target is now loaded is applied.
|
|
15640
|
+
* Selection: see `reconcileSelection` in `common/view-state/tree.ts`.
|
|
15641
|
+
*/
|
|
15642
|
+
function reconcileTreemapViewState(input) {
|
|
15643
|
+
const { index } = input;
|
|
15644
|
+
let zoom;
|
|
15645
|
+
let pendingZoom = input.pendingZoom;
|
|
15646
|
+
if (input.zoomKey !== void 0 && !index.has(input.zoomKey)) {
|
|
15647
|
+
pendingZoom ??= input.zoomPath;
|
|
15648
|
+
zoom = {
|
|
15649
|
+
key: void 0,
|
|
15650
|
+
path: [],
|
|
15651
|
+
restored: false
|
|
15652
|
+
};
|
|
15653
|
+
} else if (input.zoomKey !== void 0) {
|
|
15654
|
+
const path = treemapKeyPath(index, input.zoomKey) ?? input.zoomPath;
|
|
15655
|
+
if (!samePath(path, input.zoomPath)) zoom = {
|
|
15656
|
+
key: input.zoomKey,
|
|
15657
|
+
path,
|
|
15658
|
+
restored: false
|
|
15659
|
+
};
|
|
15660
|
+
}
|
|
15661
|
+
if (pendingZoom) {
|
|
15662
|
+
const resolved = resolveZoomPath(index, pendingZoom);
|
|
15663
|
+
if (resolved.kind === `found`) {
|
|
15664
|
+
const key = resolved.key;
|
|
15665
|
+
zoom = {
|
|
15666
|
+
key,
|
|
15667
|
+
path: key === void 0 ? [] : treemapKeyPath(index, key) ?? pendingZoom,
|
|
15668
|
+
restored: true
|
|
15669
|
+
};
|
|
15670
|
+
pendingZoom = void 0;
|
|
15671
|
+
}
|
|
15672
|
+
}
|
|
15673
|
+
const selection = reconcileSelection$1(input.selected, input.pendingSelected, (key) => index.get(key)?.node, input.canSelect, input.singleSelect);
|
|
15674
|
+
return {
|
|
15675
|
+
zoom,
|
|
15676
|
+
pendingZoom,
|
|
15677
|
+
selection
|
|
15678
|
+
};
|
|
15679
|
+
}
|
|
15680
|
+
function samePath(a, b) {
|
|
15681
|
+
return a.length === b.length && a.every((k, i) => k === b[i]);
|
|
15682
|
+
}
|
|
15683
|
+
//#endregion
|
|
15684
|
+
//#region src/treemap/treemap-view-state-binding.ts
|
|
15685
|
+
function createTreemapViewState(host) {
|
|
15686
|
+
const { controller } = host;
|
|
15687
|
+
/** Key path of the zoom root as last seen — what goes pending if it leaves the tree. */
|
|
15688
|
+
let zoomPath = [];
|
|
15689
|
+
let pendingZoom;
|
|
15690
|
+
let pendingSelected = EMPTY_KEYS;
|
|
15691
|
+
let pendingFocus;
|
|
15692
|
+
/** The cursor was last moved by keyboard / search (only then is it saved as `focus`). */
|
|
15693
|
+
let keyboardCursor = false;
|
|
15694
|
+
let structureVersion = 0;
|
|
15695
|
+
let reconciledVersion = -1;
|
|
15696
|
+
let cache;
|
|
15697
|
+
const index = () => {
|
|
15698
|
+
if (cache?.version !== structureVersion) cache = {
|
|
15699
|
+
version: structureVersion,
|
|
15700
|
+
index: indexTreemapKeys(host.root())
|
|
15701
|
+
};
|
|
15702
|
+
return cache.index;
|
|
15703
|
+
};
|
|
15704
|
+
const cursorKey = () => keyboardCursor ? host.tickled() : void 0;
|
|
15705
|
+
const applyZoom = (key, path, restored) => {
|
|
15706
|
+
zoomPath = path;
|
|
15707
|
+
if (host.zoomKey() === key) return;
|
|
15708
|
+
host.setZoom(key, restored);
|
|
15709
|
+
controller.notify();
|
|
15710
|
+
};
|
|
15711
|
+
const reconcileCursor$1 = (idx) => {
|
|
15712
|
+
const cursor = reconcileCursor(cursorKey(), pendingFocus, (key) => idx.has(key), host.isVisible);
|
|
15713
|
+
pendingFocus = cursor.pending;
|
|
15714
|
+
const tickled = host.tickled();
|
|
15715
|
+
if (cursor.clear || tickled !== void 0 && !idx.has(tickled)) host.setTickled(void 0);
|
|
15716
|
+
if (cursor.apply !== void 0) {
|
|
15717
|
+
host.setTickled(cursor.apply);
|
|
15718
|
+
keyboardCursor = true;
|
|
15719
|
+
controller.notify();
|
|
15720
|
+
}
|
|
15721
|
+
};
|
|
15722
|
+
/** Move state for missing keys to pending; apply pending state for keys now loaded. */
|
|
15723
|
+
const reconcile = () => {
|
|
15724
|
+
reconciledVersion = structureVersion;
|
|
15725
|
+
if (!host.root()) return;
|
|
15726
|
+
const idx = index();
|
|
15727
|
+
controller.restoring(() => {
|
|
15728
|
+
const r = reconcileTreemapViewState({
|
|
15729
|
+
index: idx,
|
|
15730
|
+
zoomKey: host.zoomKey(),
|
|
15731
|
+
zoomPath,
|
|
15732
|
+
pendingZoom,
|
|
15733
|
+
selected: host.selected(),
|
|
15734
|
+
pendingSelected,
|
|
15735
|
+
canSelect: host.canSelect,
|
|
15736
|
+
singleSelect: host.singleSelect()
|
|
15737
|
+
});
|
|
15738
|
+
pendingZoom = r.pendingZoom;
|
|
15739
|
+
if (r.zoom) applyZoom(r.zoom.key, r.zoom.path, r.zoom.restored);
|
|
15740
|
+
const sel = r.selection;
|
|
15741
|
+
if (sel.changed) {
|
|
15742
|
+
pendingSelected = sel.pending;
|
|
15743
|
+
const primaryKey = host.selectedNode()?.item.key;
|
|
15744
|
+
const primary = [...sel.selected].find((n) => n.item.key === primaryKey) ?? [...sel.selected].at(-1);
|
|
15745
|
+
host.setSelection(sel.selected, primary, sel.restored);
|
|
15746
|
+
}
|
|
15747
|
+
reconcileCursor$1(idx);
|
|
15748
|
+
});
|
|
15749
|
+
};
|
|
15750
|
+
return {
|
|
15751
|
+
/** The structure changed (new root / model, invalidation, lazy load). */
|
|
15752
|
+
invalidate() {
|
|
15753
|
+
structureVersion++;
|
|
15754
|
+
},
|
|
15755
|
+
/** Run in `willUpdate`. `zoomGone` — the zoom root vanished through in-place mutation. */
|
|
15756
|
+
reconcileIfNeeded(zoomGone) {
|
|
15757
|
+
if (zoomGone) structureVersion++;
|
|
15758
|
+
if (reconciledVersion !== structureVersion) reconcile();
|
|
15759
|
+
},
|
|
15760
|
+
/** An explicit zoom: supersedes a saved one still waiting. */
|
|
15761
|
+
zoomed(key) {
|
|
15762
|
+
pendingZoom = void 0;
|
|
15763
|
+
zoomPath = key === void 0 ? [] : treemapKeyPath(index(), key) ?? [key];
|
|
15764
|
+
controller.notify();
|
|
15765
|
+
},
|
|
15766
|
+
/** Selection changed — explicit changes supersede a saved selection still waiting. */
|
|
15767
|
+
selectionChanged() {
|
|
15768
|
+
if (!controller.isRestoring) pendingSelected = EMPTY_KEYS;
|
|
15769
|
+
controller.notify();
|
|
15770
|
+
},
|
|
15771
|
+
clearPendingSelection() {
|
|
15772
|
+
pendingSelected = EMPTY_KEYS;
|
|
15773
|
+
},
|
|
15774
|
+
/** The cursor moved by keyboard / search navigation. */
|
|
15775
|
+
keyboardCursorMoved() {
|
|
15776
|
+
keyboardCursor = true;
|
|
15777
|
+
pendingFocus = void 0;
|
|
15778
|
+
controller.notify();
|
|
15779
|
+
},
|
|
15780
|
+
/** The cursor moved by pointer. */
|
|
15781
|
+
pointerCursorMoved() {
|
|
15782
|
+
if (!keyboardCursor) return;
|
|
15783
|
+
keyboardCursor = false;
|
|
15784
|
+
controller.notify();
|
|
15785
|
+
},
|
|
15786
|
+
get(options) {
|
|
15787
|
+
const withPending = options?.includePending ?? true;
|
|
15788
|
+
return controller.shape({
|
|
15789
|
+
selected: keysToArray([...host.selected()].map((n) => n.item.key), withPending ? pendingSelected : []),
|
|
15790
|
+
focus: cursorKey() ?? (withPending ? pendingFocus : void 0),
|
|
15791
|
+
zoom: [...(withPending ? pendingZoom : void 0) ?? zoomPath]
|
|
15792
|
+
}, options);
|
|
15793
|
+
},
|
|
15794
|
+
set(state) {
|
|
15795
|
+
if (!controller.accepts(state)) return;
|
|
15796
|
+
controller.restoring(() => {
|
|
15797
|
+
if (state.selected) {
|
|
15798
|
+
pendingSelected = capKeys(new Set(state.selected));
|
|
15799
|
+
if (host.selected().size > 0) host.setSelection(/* @__PURE__ */ new Set(), void 0, true);
|
|
15800
|
+
}
|
|
15801
|
+
if (state.focus !== void 0) pendingFocus = state.focus;
|
|
15802
|
+
const zoom = pathFromJson(state.zoom);
|
|
15803
|
+
if (zoom) pendingZoom = zoom;
|
|
15804
|
+
reconcile();
|
|
15805
|
+
if (zoom && pendingZoom) applyZoom(void 0, [], true);
|
|
15806
|
+
});
|
|
15807
|
+
},
|
|
15808
|
+
clearPending() {
|
|
15809
|
+
pendingSelected = EMPTY_KEYS;
|
|
15810
|
+
pendingFocus = void 0;
|
|
15811
|
+
pendingZoom = void 0;
|
|
15812
|
+
},
|
|
15813
|
+
/** Load the next unloaded ancestor on a pending zoom path (one load at a time). */
|
|
15814
|
+
continueRestoreLoads() {
|
|
15815
|
+
if (!pendingZoom || !host.restoreLoads() || !host.canLoad()) return;
|
|
15816
|
+
const resolved = resolveZoomPath(index(), pendingZoom);
|
|
15817
|
+
if (resolved.kind === `load` && !host.isLoading(resolved.node.item.key)) host.load(resolved.node, resolved.depth);
|
|
15818
|
+
}
|
|
15819
|
+
};
|
|
15820
|
+
}
|
|
15821
|
+
//#endregion
|
|
14539
15822
|
//#region src/treemap/treemap-list.ts
|
|
14540
15823
|
let TreemapListElement = class TreemapListElement extends LitElement {
|
|
14541
15824
|
constructor(..._args) {
|
|
@@ -14557,6 +15840,33 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14557
15840
|
this.disableKeyboardNavigation = false;
|
|
14558
15841
|
this.registry = void 0;
|
|
14559
15842
|
this._zoomKey = void 0;
|
|
15843
|
+
this.restoreLoads = true;
|
|
15844
|
+
this.#viewState = createTreemapViewState({
|
|
15845
|
+
controller: new ViewStateController(this, {
|
|
15846
|
+
kind: `treemap-list`,
|
|
15847
|
+
capture: () => this.getViewState()
|
|
15848
|
+
}),
|
|
15849
|
+
root: () => this.#effectiveRoot,
|
|
15850
|
+
zoomKey: () => this._zoomKey,
|
|
15851
|
+
setZoom: (key, restored) => this.#restoreZoom(key, restored),
|
|
15852
|
+
selected: () => this._selectedNodes,
|
|
15853
|
+
selectedNode: () => this.selectedNode,
|
|
15854
|
+
canSelect: (node) => this.#canSelect(node),
|
|
15855
|
+
singleSelect: () => this.interactionMode === `implicit`,
|
|
15856
|
+
setSelection: (next, primary, fire) => {
|
|
15857
|
+
const prev = this._selectedNodes;
|
|
15858
|
+
this._selectedNodes = new Set(next);
|
|
15859
|
+
this.selectedNode = primary;
|
|
15860
|
+
if (fire) this.#dispatchSelect(prev);
|
|
15861
|
+
},
|
|
15862
|
+
tickled: () => this.tickled.tickledId,
|
|
15863
|
+
setTickled: (key) => key === void 0 ? this.tickled.clearTickled() : this.tickled.setTickled(key),
|
|
15864
|
+
isVisible: (key) => this.#rects().some((r) => r.node.item.key === key),
|
|
15865
|
+
restoreLoads: () => this.restoreLoads,
|
|
15866
|
+
canLoad: () => this.model !== void 0 || this.loadChildren !== void 0,
|
|
15867
|
+
isLoading: (key) => this.#pendingKeys.has(key),
|
|
15868
|
+
load: (node, depth) => void this.#loadChildren(node, depth)
|
|
15869
|
+
});
|
|
14560
15870
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
14561
15871
|
labelSelector: `.treemap-label`,
|
|
14562
15872
|
highlightKey: `incr-search-highlight`
|
|
@@ -14595,6 +15905,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14595
15905
|
if (this._zoomKey === void 0) return void 0;
|
|
14596
15906
|
return findTreemapNode(this.#effectiveRoot, this._zoomKey);
|
|
14597
15907
|
}
|
|
15908
|
+
#viewState;
|
|
14598
15909
|
#modelUnsubscribe;
|
|
14599
15910
|
#pendingKeys;
|
|
14600
15911
|
#aborters;
|
|
@@ -14690,40 +16001,38 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14690
16001
|
this.#unregisterSearchCommands?.();
|
|
14691
16002
|
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
14692
16003
|
}
|
|
16004
|
+
willUpdate(changedProps) {
|
|
16005
|
+
if (changedProps.has(`root`) && this.root || changedProps.has(`model`)) this.#onDataReset();
|
|
16006
|
+
this.#viewState.reconcileIfNeeded(this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey));
|
|
16007
|
+
}
|
|
14693
16008
|
updated(changedProps) {
|
|
14694
|
-
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
14695
16009
|
if (changedProps.has(`model`)) {
|
|
14696
16010
|
this.#modelUnsubscribe?.();
|
|
14697
16011
|
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
|
-
}
|
|
16012
|
+
if (this.model) this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
16013
|
+
if (event.type === `root`) this.#onDataReset();
|
|
16014
|
+
else if (event.type === `invalidate`) this.#viewState.invalidate();
|
|
16015
|
+
this.requestUpdate();
|
|
16016
|
+
});
|
|
14707
16017
|
}
|
|
14708
|
-
if (changedProps.has(`interactionMode`)) {
|
|
16018
|
+
if (changedProps.has(`interactionMode`) && changedProps.get(`interactionMode`) !== void 0) {
|
|
14709
16019
|
this.#anchorIndex = void 0;
|
|
14710
16020
|
this.clearSelection();
|
|
14711
16021
|
}
|
|
14712
16022
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
14713
|
-
if (this._zoomKey !== void 0 && !findTreemapNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
14714
16023
|
this.#ensureVisibleChildren();
|
|
16024
|
+
this.#viewState.continueRestoreLoads();
|
|
14715
16025
|
if (this.#pendingFlip) {
|
|
14716
16026
|
const flip = this.#pendingFlip;
|
|
14717
16027
|
this.#pendingFlip = void 0;
|
|
14718
16028
|
this.#applyFlip(flip);
|
|
14719
16029
|
}
|
|
14720
16030
|
}
|
|
14721
|
-
|
|
16031
|
+
/** New `root` / `model`: drop transient state; keyed view state is reconciled, not reset. */
|
|
16032
|
+
#onDataReset() {
|
|
14722
16033
|
this.#pendingFlip = void 0;
|
|
14723
|
-
this.#clearSelectionInternal();
|
|
14724
|
-
this.tickled.clearTickled();
|
|
14725
|
-
this._zoomKey = void 0;
|
|
14726
16034
|
this.#anchorIndex = void 0;
|
|
16035
|
+
this.#viewState.invalidate();
|
|
14727
16036
|
}
|
|
14728
16037
|
#ensureVisibleChildren() {
|
|
14729
16038
|
if (!this.model && !this.loadChildren) return;
|
|
@@ -14754,7 +16063,10 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14754
16063
|
} finally {
|
|
14755
16064
|
this.#pendingKeys.delete(key);
|
|
14756
16065
|
this.#aborters.delete(key);
|
|
14757
|
-
if (!controller.signal.aborted)
|
|
16066
|
+
if (!controller.signal.aborted) {
|
|
16067
|
+
this.#viewState.invalidate();
|
|
16068
|
+
this.requestUpdate();
|
|
16069
|
+
}
|
|
14758
16070
|
}
|
|
14759
16071
|
}
|
|
14760
16072
|
#canSelect(node) {
|
|
@@ -14768,6 +16080,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14768
16080
|
this.selectedNode = void 0;
|
|
14769
16081
|
}
|
|
14770
16082
|
#dispatchSelect(previous) {
|
|
16083
|
+
this.#viewState.selectionChanged();
|
|
14771
16084
|
this.dispatchEvent(new CustomEvent(`select`, {
|
|
14772
16085
|
detail: {
|
|
14773
16086
|
selected: new Set(this._selectedNodes),
|
|
@@ -14795,6 +16108,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14795
16108
|
this.requestUpdate();
|
|
14796
16109
|
}
|
|
14797
16110
|
clearSelection() {
|
|
16111
|
+
this.#viewState.clearPendingSelection();
|
|
14798
16112
|
if (this._selectedNodes.size === 0) return;
|
|
14799
16113
|
const prev = new Set(this._selectedNodes);
|
|
14800
16114
|
this.#clearSelectionInternal();
|
|
@@ -14833,7 +16147,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14833
16147
|
zoomTo(node) {
|
|
14834
16148
|
if (this._zoomKey === node.item.key) return;
|
|
14835
16149
|
this.#pendingFlip = this.#planFlip(node.item.key);
|
|
14836
|
-
this
|
|
16150
|
+
this.#setZoom(node.item.key);
|
|
14837
16151
|
this.tickled.clearTickled();
|
|
14838
16152
|
const depth = findTreemapPath(this.#effectiveRoot, node.item.key)?.length;
|
|
14839
16153
|
const absoluteDepth = depth === void 0 ? -1 : depth - 2;
|
|
@@ -14865,7 +16179,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14865
16179
|
const previous = this.focusedNode;
|
|
14866
16180
|
const path = this._zoomKey === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, this._zoomKey);
|
|
14867
16181
|
const absoluteDepth = path === void 0 ? -1 : path.length - 2;
|
|
14868
|
-
this
|
|
16182
|
+
this.#setZoom(void 0);
|
|
14869
16183
|
this.tickled.clearTickled();
|
|
14870
16184
|
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
14871
16185
|
detail: {
|
|
@@ -14885,6 +16199,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14885
16199
|
}));
|
|
14886
16200
|
this.requestUpdate();
|
|
14887
16201
|
}
|
|
16202
|
+
/** An explicit zoom change: supersedes any saved zoom still waiting. */
|
|
16203
|
+
#setZoom(key) {
|
|
16204
|
+
this._zoomKey = key;
|
|
16205
|
+
this.#viewState.zoomed(key);
|
|
16206
|
+
}
|
|
16207
|
+
/** Zoom set by view-state reconcile. `restored` fires `zoom` like an explicit zoom, without animating. */
|
|
16208
|
+
#restoreZoom(key, restored) {
|
|
16209
|
+
this._zoomKey = key;
|
|
16210
|
+
if (!restored) return;
|
|
16211
|
+
const path = key === void 0 ? void 0 : findTreemapPath(this.#effectiveRoot, key);
|
|
16212
|
+
this.dispatchEvent(new CustomEvent(`zoom`, {
|
|
16213
|
+
detail: {
|
|
16214
|
+
node: path?.at(-1),
|
|
16215
|
+
depth: path === void 0 ? -1 : path.length - 2
|
|
16216
|
+
},
|
|
16217
|
+
bubbles: true,
|
|
16218
|
+
composed: true
|
|
16219
|
+
}));
|
|
16220
|
+
}
|
|
14888
16221
|
/**
|
|
14889
16222
|
* Work out (before `_zoomKey` changes) whether the upcoming zoom should
|
|
14890
16223
|
* animate, and how — see `FlipPlan`. Returns `undefined` to skip the
|
|
@@ -14994,10 +16327,20 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
14994
16327
|
const ancestor = this.#resolveKeyPath(path.slice(0, path.length - this.maxDepth));
|
|
14995
16328
|
if (ancestor) this.zoomTo(ancestor);
|
|
14996
16329
|
}
|
|
14997
|
-
this
|
|
16330
|
+
this.#setKeyboardCursor(key);
|
|
14998
16331
|
this.requestUpdate();
|
|
14999
16332
|
return true;
|
|
15000
16333
|
}
|
|
16334
|
+
/** Move the cursor as keyboard / search navigation does — recorded as `focus` in the view state. */
|
|
16335
|
+
#setKeyboardCursor(key) {
|
|
16336
|
+
this.tickled.setTickled(key);
|
|
16337
|
+
this.#viewState.keyboardCursorMoved();
|
|
16338
|
+
}
|
|
16339
|
+
#onTilePointerEnter(key) {
|
|
16340
|
+
const before = this.tickled.tickledId;
|
|
16341
|
+
this.tickled.handlePointerEnter(key);
|
|
16342
|
+
if (this.tickled.tickledId !== before) this.#viewState.pointerCursorMoved();
|
|
16343
|
+
}
|
|
15001
16344
|
#resolveKeyPath(keys) {
|
|
15002
16345
|
let current = this.#effectiveRoot;
|
|
15003
16346
|
for (const key of keys) {
|
|
@@ -15027,6 +16370,25 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15027
16370
|
depth: r.depth
|
|
15028
16371
|
}));
|
|
15029
16372
|
}
|
|
16373
|
+
/**
|
|
16374
|
+
* Snapshot selection, zoom path and keyboard cursor as plain JSON, keyed by
|
|
16375
|
+
* `item.key`. Includes pending state for keys not currently loaded.
|
|
16376
|
+
*/
|
|
16377
|
+
getViewState(options) {
|
|
16378
|
+
return this.#viewState.get(options);
|
|
16379
|
+
}
|
|
16380
|
+
/**
|
|
16381
|
+
* Apply saved view state. Fields present replace the current ones; keys
|
|
16382
|
+
* that aren't loaded yet are applied as they appear — a deep `zoom` loads
|
|
16383
|
+
* its ancestors via `loadChildren` / `model` when `restoreLoads`.
|
|
16384
|
+
*/
|
|
16385
|
+
setViewState(state) {
|
|
16386
|
+
this.#viewState.set(state);
|
|
16387
|
+
this.requestUpdate();
|
|
16388
|
+
}
|
|
16389
|
+
clearPendingViewState() {
|
|
16390
|
+
this.#viewState.clearPending();
|
|
16391
|
+
}
|
|
15030
16392
|
#matchesFilter(node, depth) {
|
|
15031
16393
|
if (!this.filterPredicate) return true;
|
|
15032
16394
|
if (this.filterPredicate(node, depth)) return true;
|
|
@@ -15128,7 +16490,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15128
16490
|
const moveTo = (rect) => {
|
|
15129
16491
|
if (!rect) return;
|
|
15130
16492
|
event.preventDefault();
|
|
15131
|
-
this
|
|
16493
|
+
this.#setKeyboardCursor(rect.node.item.key);
|
|
15132
16494
|
if (this.interactionMode === `implicit`) this.select(rect.node);
|
|
15133
16495
|
};
|
|
15134
16496
|
switch (event.key) {
|
|
@@ -15309,7 +16671,7 @@ let TreemapListElement = class TreemapListElement extends LitElement {
|
|
|
15309
16671
|
e.stopPropagation();
|
|
15310
16672
|
this.#handleItemDblClick(rect);
|
|
15311
16673
|
}}
|
|
15312
|
-
@pointerenter=${() => this
|
|
16674
|
+
@pointerenter=${() => this.#onTilePointerEnter(node.item.key)}
|
|
15313
16675
|
>
|
|
15314
16676
|
${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
16677
|
${childrenContent}
|
|
@@ -15623,6 +16985,7 @@ __decorate([property({
|
|
|
15623
16985
|
reflect: true
|
|
15624
16986
|
})], TreemapListElement.prototype, "searchModes", void 0);
|
|
15625
16987
|
__decorate([state()], TreemapListElement.prototype, "_zoomKey", void 0);
|
|
16988
|
+
__decorate([property({ attribute: false })], TreemapListElement.prototype, "restoreLoads", void 0);
|
|
15626
16989
|
TreemapListElement = __decorate([safeCustomElement(`ixfx-treemap-list`)], TreemapListElement);
|
|
15627
16990
|
//#endregion
|
|
15628
16991
|
//#region src/twosplit-layout/split-layout.ts
|
|
@@ -15649,6 +17012,10 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15649
17012
|
this.#nestedSplitInitialSizeA = ``;
|
|
15650
17013
|
this.#pendingNestedUpdate = null;
|
|
15651
17014
|
this.#hysteresisThreshold = .1;
|
|
17015
|
+
this.#viewState = new ViewStateController(this, {
|
|
17016
|
+
kind: `twosplit-layout`,
|
|
17017
|
+
capture: () => this.getViewState()
|
|
17018
|
+
});
|
|
15652
17019
|
this.#onResizeHandleDown = (event) => {
|
|
15653
17020
|
event.preventDefault();
|
|
15654
17021
|
const zoneEl = this.getZone();
|
|
@@ -15665,6 +17032,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15665
17032
|
if (zoneEl) zoneEl.classList.remove(`user-resizing`);
|
|
15666
17033
|
window.removeEventListener(`pointerup`, this.#onResizeHandleUp);
|
|
15667
17034
|
window.removeEventListener(`pointermove`, this.#onResizeHandleMove);
|
|
17035
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15668
17036
|
};
|
|
15669
17037
|
this.#onResizeHandleMove = (event) => {
|
|
15670
17038
|
const bounds = this.#elementBounds;
|
|
@@ -15784,9 +17152,11 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15784
17152
|
if (nestedSplit) {
|
|
15785
17153
|
const nestedZoneEl = nestedSplit.getZone();
|
|
15786
17154
|
if (nestedZoneEl) nestedZoneEl.classList.remove(`user-resizing`);
|
|
17155
|
+
if (#viewState in nestedSplit && nestedSplit.sizeA !== this.#nestedSplitInitialSizeA) nestedSplit.#viewState.notify();
|
|
15787
17156
|
}
|
|
15788
17157
|
window.removeEventListener(`pointerup`, this.#onCornerResizeUp);
|
|
15789
17158
|
window.removeEventListener(`pointermove`, this.#onCornerResizeMove);
|
|
17159
|
+
if (this.sizeA !== this.#startSizeA || this.sizeB !== this.#startSizeB) this.#viewState.notify();
|
|
15790
17160
|
};
|
|
15791
17161
|
this.#onCornerResizeMove = (event) => {
|
|
15792
17162
|
const outerBounds = this.#elementBounds;
|
|
@@ -15854,6 +17224,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15854
17224
|
#collapsedAPointerAtCollapse;
|
|
15855
17225
|
#collapsedBPointerAtCollapse;
|
|
15856
17226
|
#hysteresisThreshold;
|
|
17227
|
+
#viewState;
|
|
15857
17228
|
disconnectedCallback() {
|
|
15858
17229
|
super.disconnectedCallback();
|
|
15859
17230
|
this.#observer?.disconnect();
|
|
@@ -15863,7 +17234,7 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
15863
17234
|
}
|
|
15864
17235
|
updated(changedProperties) {
|
|
15865
17236
|
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();
|
|
17237
|
+
if (changedProperties.has(`sizeA`) || changedProperties.has(`sizeB`) || changedProperties.has(`collapsedA`) || changedProperties.has(`collapsedB`)) this.#updateDataCollapsed();
|
|
15867
17238
|
}
|
|
15868
17239
|
#updateDataCollapsed() {
|
|
15869
17240
|
if (this.collapsedA) this.setAttribute(`data-collapsed-a`, ``);
|
|
@@ -16022,6 +17393,31 @@ let TwoSplitLayoutElement = class TwoSplitLayoutElement extends LitElement {
|
|
|
16022
17393
|
else if (this.layout === `horizontal` && nestedSplit.layout === `vertical`) cornerZone.style.cursor = `nesw-resize`;
|
|
16023
17394
|
}
|
|
16024
17395
|
}
|
|
17396
|
+
/** Snapshot both sides' size and collapse. */
|
|
17397
|
+
getViewState(options) {
|
|
17398
|
+
return this.#viewState.shape({ layout: {
|
|
17399
|
+
a: sideState(this.sizeA, this.collapsedA),
|
|
17400
|
+
b: sideState(this.sizeB, this.collapsedB)
|
|
17401
|
+
} }, options);
|
|
17402
|
+
}
|
|
17403
|
+
/** Restore sizes / collapse. A side missing from `layout` is left as it is. */
|
|
17404
|
+
setViewState(state) {
|
|
17405
|
+
if (!this.#viewState.accepts(state) || !state.layout) return;
|
|
17406
|
+
const { a, b } = state.layout;
|
|
17407
|
+
this.#viewState.restoring(() => {
|
|
17408
|
+
if (typeof a?.size === `string`) {
|
|
17409
|
+
this.sizeA = a.size;
|
|
17410
|
+
this.collapsedA = a.collapsed === true;
|
|
17411
|
+
}
|
|
17412
|
+
if (typeof b?.size === `string`) {
|
|
17413
|
+
this.sizeB = b.size;
|
|
17414
|
+
this.collapsedB = b.collapsed === true;
|
|
17415
|
+
}
|
|
17416
|
+
this.#viewState.notify();
|
|
17417
|
+
});
|
|
17418
|
+
}
|
|
17419
|
+
/** Both slots always exist, so nothing is ever pending. */
|
|
17420
|
+
clearPendingViewState() {}
|
|
16025
17421
|
getZone() {
|
|
16026
17422
|
if (!this.shadowRoot) return;
|
|
16027
17423
|
const el = this.shadowRoot.querySelector(`#zone`);
|
|
@@ -16209,6 +17605,12 @@ __decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "minSi
|
|
|
16209
17605
|
__decorate([property({ type: String })], TwoSplitLayoutElement.prototype, "maxSizeB", void 0);
|
|
16210
17606
|
__decorate([state()], TwoSplitLayoutElement.prototype, "_hasCorner", void 0);
|
|
16211
17607
|
TwoSplitLayoutElement = __decorate([safeCustomElement(`ixfx-twosplit-layout`)], TwoSplitLayoutElement);
|
|
17608
|
+
function sideState(size, collapsed) {
|
|
17609
|
+
return collapsed ? {
|
|
17610
|
+
size,
|
|
17611
|
+
collapsed
|
|
17612
|
+
} : { size };
|
|
17613
|
+
}
|
|
16212
17614
|
//#endregion
|
|
16213
17615
|
//#region src/util/checkbox.ts
|
|
16214
17616
|
function isCheckboxLike(el) {
|
|
@@ -16267,4 +17669,4 @@ function init() {
|
|
|
16267
17669
|
console.log(`Init`);
|
|
16268
17670
|
}
|
|
16269
17671
|
//#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 };
|
|
17672
|
+
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 };
|