@ixfx/components 0.7.3 → 0.7.5
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 +1355 -526
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +3277 -384
- package/bundle/index.js.map +1 -1
- package/dist/ac-text.d.ts +1 -1
- package/dist/ac-text.js +1 -1
- package/dist/{ac-token-TCLAZeVg.d.ts → ac-token--_vCHXWw.d.ts} +2 -2
- package/dist/{ac-token-TCLAZeVg.d.ts.map → ac-token--_vCHXWw.d.ts.map} +1 -1
- package/dist/ac-token.d.ts +1 -1
- package/dist/ac-token.js +2 -2
- package/dist/button.d.ts +1 -1
- package/dist/button.d.ts.map +1 -1
- package/dist/colour-picker.d.ts +1 -1
- package/dist/crumbs.d.ts +6 -2
- package/dist/crumbs.d.ts.map +1 -1
- package/dist/crumbs.js +13 -3
- package/dist/crumbs.js.map +1 -1
- package/dist/{data-grid-Db4QpnII.d.ts → data-grid-CtJ35v8L.d.ts} +59 -2
- package/dist/data-grid-CtJ35v8L.d.ts.map +1 -0
- package/dist/data-grid.d.ts +1 -1
- package/dist/data-grid.js +268 -14
- package/dist/data-grid.js.map +1 -1
- package/dist/detail-list-BS5dRDda.d.ts +175 -0
- package/dist/detail-list-BS5dRDda.d.ts.map +1 -0
- package/dist/detail-list.d.ts +3 -0
- package/dist/detail-list.js +1031 -0
- package/dist/detail-list.js.map +1 -0
- package/dist/grid-list-D-GbghSH.d.ts +321 -0
- package/dist/grid-list-D-GbghSH.d.ts.map +1 -0
- package/dist/grid-list.d.ts +3 -0
- package/dist/grid-list.js +994 -0
- package/dist/grid-list.js.map +1 -0
- package/dist/{grouped-item-lister-BWlC9S4e.d.ts → grouped-item-lister-fdZHVxFM.d.ts} +3 -3
- package/dist/{grouped-item-lister-BWlC9S4e.d.ts.map → grouped-item-lister-fdZHVxFM.d.ts.map} +1 -1
- package/dist/grouped-item-lister.d.ts +1 -1
- package/dist/{icon-DR_3rSyZ.d.ts → icon-CDGprSdE.d.ts} +2 -2
- package/dist/icon-CDGprSdE.d.ts.map +1 -0
- package/dist/icons.d.ts +2 -2
- package/dist/incr-search-DpbSJtgj.js +962 -0
- package/dist/incr-search-DpbSJtgj.js.map +1 -0
- package/dist/incr-search.d.ts +2 -2
- package/dist/incr-search.js +2 -3
- package/dist/{index-C59-JEPl.d.ts → index-9AhaHIT2.d.ts} +3 -3
- package/dist/{index-C59-JEPl.d.ts.map → index-9AhaHIT2.d.ts.map} +1 -1
- package/dist/index-CBS4z1v-.d.ts +365 -0
- package/dist/index-CBS4z1v-.d.ts.map +1 -0
- package/dist/{index-DsjpDksO.d.ts → index-CS9yM8L-.d.ts} +2 -2
- package/dist/{index-DsjpDksO.d.ts.map → index-CS9yM8L-.d.ts.map} +1 -1
- package/dist/{index-BUB5SICW.d.ts → index-CoqtMSd9.d.ts} +2 -2
- package/dist/{index-BUB5SICW.d.ts.map → index-CoqtMSd9.d.ts.map} +1 -1
- package/dist/{index-CJ5fshD3.d.ts → index-UvnoZUAo.d.ts} +2 -2
- package/dist/{index-CJ5fshD3.d.ts.map → index-UvnoZUAo.d.ts.map} +1 -1
- package/dist/{index-BOUaDIW-.d.ts → index-tq6f8007.d.ts} +2 -2
- package/dist/{index-BOUaDIW-.d.ts.map → index-tq6f8007.d.ts.map} +1 -1
- package/dist/index.d.ts +188 -243
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +376 -1004
- package/dist/index.js.map +1 -1
- package/dist/interaction-Cr13azbM.js +3 -0
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts → labelled-input-base-DiTme1JO.d.ts} +2 -2
- package/dist/{labelled-input-base-Dp_9KP0G.d.ts.map → labelled-input-base-DiTme1JO.d.ts.map} +1 -1
- package/dist/labelled-radial-input.d.ts +2 -2
- package/dist/labelled-radial-input.d.ts.map +1 -1
- package/dist/labelled-range-input.d.ts +1 -1
- package/dist/labelled-slider-input.d.ts +1 -1
- package/dist/list-item-animation-DuiwgN3l.js +112 -0
- package/dist/list-item-animation-DuiwgN3l.js.map +1 -0
- package/dist/list-selection-controller-DTFI40li.js +264 -0
- package/dist/list-selection-controller-DTFI40li.js.map +1 -0
- package/dist/{list-selection-types-DSuRNWpx.d.ts → list-selection-types-B5ioL-dG.d.ts} +6 -1
- package/dist/list-selection-types-B5ioL-dG.d.ts.map +1 -0
- package/dist/{menu-DMzjkVnK.js → menu-Bg90Qr0B.js} +3 -3
- package/dist/{menu-DMzjkVnK.js.map → menu-Bg90Qr0B.js.map} +1 -1
- package/dist/{menu-item-D2S4i8u1.js → menu-item-Bx-Fs7jq.js} +3 -3
- package/dist/{menu-item-D2S4i8u1.js.map → menu-item-Bx-Fs7jq.js.map} +1 -1
- package/dist/{menu-item-ZmkXNjek.d.ts → menu-item-CZi9vy5O.d.ts} +2 -2
- package/dist/{menu-item-ZmkXNjek.d.ts.map → menu-item-CZi9vy5O.d.ts.map} +1 -1
- package/dist/menu.d.ts +2 -2
- package/dist/menu.js +2 -2
- package/dist/miller.d.ts +82 -4
- package/dist/miller.d.ts.map +1 -1
- package/dist/miller.js +329 -70
- package/dist/miller.js.map +1 -1
- package/dist/narrowed-text.js +3 -4
- package/dist/narrowed-text.js.map +1 -1
- package/dist/panel.d.ts +1 -1
- package/dist/plots.js +1 -1
- package/dist/select-horiz.js +2 -2
- package/dist/{split-layout-C5SYbDrq.d.ts → split-layout-JK3xWVX2.d.ts} +2 -2
- package/dist/{split-layout-C5SYbDrq.d.ts.map → split-layout-JK3xWVX2.d.ts.map} +1 -1
- package/dist/split-layout.d.ts +1 -1
- package/dist/{tab-list-DbMIL520.d.ts → tab-list-CGyPt07Z.d.ts} +2 -2
- package/dist/{tab-list-DbMIL520.d.ts.map → tab-list-CGyPt07Z.d.ts.map} +1 -1
- package/dist/tabs.d.ts +1 -1
- package/dist/{tickled-controller-Ds1DMyG7.d.ts → tickled-controller-Bn3YdQsM.d.ts} +12 -1
- package/dist/{tickled-controller-Ds1DMyG7.d.ts.map → tickled-controller-Bn3YdQsM.d.ts.map} +1 -1
- package/dist/{tickled-controller-h9GmJ_bU.js → tickled-controller-C66DgJHl.js} +16 -1
- package/dist/tickled-controller-C66DgJHl.js.map +1 -0
- package/dist/{titlebar-B23MrIbI.d.ts → titlebar-Bj5pfdw_.d.ts} +2 -2
- package/dist/{titlebar-B23MrIbI.d.ts.map → titlebar-Bj5pfdw_.d.ts.map} +1 -1
- package/dist/titlebar.d.ts +1 -1
- package/dist/{tree-Ch4W8qXQ.js → tree-DAii2A1g.js} +190 -29
- package/dist/tree-DAii2A1g.js.map +1 -0
- package/dist/{tree-component-BvV4muxx.d.ts → tree-component-F6qZ0Gdv.d.ts} +9 -1
- package/dist/{tree-component-BvV4muxx.d.ts.map → tree-component-F6qZ0Gdv.d.ts.map} +1 -1
- package/dist/tree.d.ts +28 -4
- package/dist/tree.d.ts.map +1 -1
- package/dist/tree.js +1 -1
- package/dist/vertical-list-DwtCPy1b.d.ts +143 -0
- package/dist/vertical-list-DwtCPy1b.d.ts.map +1 -0
- package/dist/vertical-list.d.ts +3 -121
- package/dist/vertical-list.js +642 -1
- package/dist/vertical-list.js.map +1 -0
- package/dist/{xy-axis-DWizs0bt.js → xy-axis-VrX8dmvS.js} +2 -2
- package/dist/{xy-axis-DWizs0bt.js.map → xy-axis-VrX8dmvS.js.map} +1 -1
- package/docs-user/README.md +1 -0
- package/docs-user/data-grid.md +70 -2
- package/docs-user/detail-list.md +39 -1
- package/docs-user/grid-list.md +298 -0
- package/docs-user/incr-search.md +12 -2
- package/docs-user/index.json +7 -2
- package/docs-user/llms.txt +1 -0
- package/docs-user/tree.md +35 -1
- package/docs-user/vertical-list.md +58 -2
- package/llms.txt +1 -0
- package/package.json +17 -2
- package/dist/data-grid-Db4QpnII.d.ts.map +0 -1
- package/dist/element-search-Y8SRfPGM.js +0 -185
- package/dist/element-search-Y8SRfPGM.js.map +0 -1
- package/dist/icon-DR_3rSyZ.d.ts.map +0 -1
- package/dist/incr-search-vCzuao-2.js +0 -137
- package/dist/incr-search-vCzuao-2.js.map +0 -1
- package/dist/index-Cu5i8zZY.d.ts +0 -160
- package/dist/index-Cu5i8zZY.d.ts.map +0 -1
- package/dist/interaction-D5XdKzbR.js +0 -2
- package/dist/list-selection-types-DSuRNWpx.d.ts.map +0 -1
- package/dist/tickled-controller-h9GmJ_bU.js.map +0 -1
- package/dist/tree-Ch4W8qXQ.js.map +0 -1
- package/dist/vertical-list-B4w_ksJk.js +0 -814
- package/dist/vertical-list-B4w_ksJk.js.map +0 -1
- package/dist/vertical-list.d.ts.map +0 -1
package/bundle/index.js
CHANGED
|
@@ -8,8 +8,9 @@ import { createCommandRegistry } from "@clinth/ui-commands";
|
|
|
8
8
|
import { Trees } from "@ixfx/collections";
|
|
9
9
|
import { clamp, clamper, rangeStream, round, scaler } from "@ixfx/numbers";
|
|
10
10
|
import { repeat } from "lit/directives/repeat.js";
|
|
11
|
-
import {
|
|
11
|
+
import { createRef, ref } from "lit/directives/ref.js";
|
|
12
12
|
import { classMap } from "lit/directives/class-map.js";
|
|
13
|
+
import { Easings } from "@ixfx/modulation";
|
|
13
14
|
import "@clinth/vcanvas";
|
|
14
15
|
import { keyed } from "lit/directives/keyed.js";
|
|
15
16
|
//#region node_modules/.pnpm/@lit+reactive-element@2.1.2/node_modules/@lit/reactive-element/css-tag.js
|
|
@@ -752,6 +753,21 @@ var TickledController = class TickledController {
|
|
|
752
753
|
return this._tickledId;
|
|
753
754
|
}
|
|
754
755
|
/**
|
|
756
|
+
* The item currently under the cursor, or `undefined`. Alias of `tickledId`
|
|
757
|
+
* for parity with other controllers.
|
|
758
|
+
*/
|
|
759
|
+
get current() {
|
|
760
|
+
return this._tickledId;
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* Move the cursor programmatically (e.g. to reveal a match or re-seat it
|
|
764
|
+
* after the item set changed). Does not change the input mode. Alias of
|
|
765
|
+
* `setTickled`.
|
|
766
|
+
*/
|
|
767
|
+
focus(id) {
|
|
768
|
+
this.setTickled(id);
|
|
769
|
+
}
|
|
770
|
+
/**
|
|
755
771
|
* Check if a specific item is tickled
|
|
756
772
|
*/
|
|
757
773
|
isTickled(id) {
|
|
@@ -2384,14 +2400,15 @@ function createIncrSearch(options = {}) {
|
|
|
2384
2400
|
}
|
|
2385
2401
|
//#endregion
|
|
2386
2402
|
//#region src/incr-search/controller-tree.ts
|
|
2387
|
-
const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
|
|
2403
|
+
const DEFAULT_HIGHLIGHT_KEY$1 = `incr-search-highlight`;
|
|
2388
2404
|
/**
|
|
2389
|
-
*
|
|
2390
|
-
* (i.e. `ixfx-tree-list`, `ixfx-miller-list`, `ixfx-crumb-navigation`).
|
|
2405
|
+
* External-input filter-search controller for `TreeSearchHost` components.
|
|
2391
2406
|
*
|
|
2392
|
-
*
|
|
2393
|
-
*
|
|
2394
|
-
*
|
|
2407
|
+
* @deprecated `ixfx-tree-list` / `ixfx-miller-list` now have a built-in
|
|
2408
|
+
* incremental-search popover with navigate / select / filter modes — invoke
|
|
2409
|
+
* `el.registry.invoke('list.search.filter')` (see `docs/infra-incremental.md`)
|
|
2410
|
+
* instead of wiring an external `<input>`. This class remains for existing
|
|
2411
|
+
* consumers and covers filter mode only.
|
|
2395
2412
|
*
|
|
2396
2413
|
* @example Using with ixfx-tree-list:
|
|
2397
2414
|
* ```typescript
|
|
@@ -2445,7 +2462,7 @@ var IncrSearchTreeController = class {
|
|
|
2445
2462
|
this.#opts.input.removeEventListener(`keydown`, this.#onKeydown);
|
|
2446
2463
|
}
|
|
2447
2464
|
this.#el.filterPredicate = void 0;
|
|
2448
|
-
clearHighlights({ key: this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY });
|
|
2465
|
+
clearHighlights({ key: this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1 });
|
|
2449
2466
|
this.#labelMap.clear();
|
|
2450
2467
|
}
|
|
2451
2468
|
async setQuery(query) {
|
|
@@ -2468,7 +2485,7 @@ var IncrSearchTreeController = class {
|
|
|
2468
2485
|
const matched = new Set(state.results.map((r) => r.item.node.item.key));
|
|
2469
2486
|
const hasQuery = state.query.trim().length > 0;
|
|
2470
2487
|
this.#el.filterPredicate = hasQuery && matched.size > 0 ? (node, _depth) => matched.has(node.item.key) : void 0;
|
|
2471
|
-
const key = this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
|
|
2488
|
+
const key = this.#opts.highlightKey ?? DEFAULT_HIGHLIGHT_KEY$1;
|
|
2472
2489
|
if (!this.#el.shadowRoot) return;
|
|
2473
2490
|
if (!state.query.trim()) {
|
|
2474
2491
|
clearHighlights({ key });
|
|
@@ -2482,11 +2499,149 @@ var IncrSearchTreeController = class {
|
|
|
2482
2499
|
}
|
|
2483
2500
|
};
|
|
2484
2501
|
//#endregion
|
|
2502
|
+
//#region src/interaction/keyed-list.ts
|
|
2503
|
+
/**
|
|
2504
|
+
* Helpers for key-based selection on the Element-flavoured list components
|
|
2505
|
+
* (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-grid-list`).
|
|
2506
|
+
*
|
|
2507
|
+
* A "key" is a stable string the host puts on each item element — `data-key`
|
|
2508
|
+
* (preferred) or `data-value`. It survives the host rebuilding the item set
|
|
2509
|
+
* (re-fetch, re-sort, filter) where raw `Element` references do not.
|
|
2510
|
+
*/
|
|
2511
|
+
/**
|
|
2512
|
+
* The stable key for an item element: `data-key`, then `data-value`, else
|
|
2513
|
+
* `undefined`.
|
|
2514
|
+
*/
|
|
2515
|
+
function keyForElement(el) {
|
|
2516
|
+
return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? void 0;
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* A lazily-built key → element index over a container. Rebuilt on first use
|
|
2520
|
+
* and after any child-list mutation under `root`; `invalidate()` forces a
|
|
2521
|
+
* rebuild on the next `get()`.
|
|
2522
|
+
*
|
|
2523
|
+
* ```ts
|
|
2524
|
+
* #keyIndex = new KeyedElementIndex(this);
|
|
2525
|
+
* // ...
|
|
2526
|
+
* const el = this.#keyIndex.get(`row-42`);
|
|
2527
|
+
* // on disconnect:
|
|
2528
|
+
* this.#keyIndex.dispose();
|
|
2529
|
+
* ```
|
|
2530
|
+
*/
|
|
2531
|
+
var KeyedElementIndex = class {
|
|
2532
|
+
#root;
|
|
2533
|
+
#selector;
|
|
2534
|
+
#map = /* @__PURE__ */ new Map();
|
|
2535
|
+
#dirty = true;
|
|
2536
|
+
#observer;
|
|
2537
|
+
/**
|
|
2538
|
+
* @param root Container whose descendants are indexed.
|
|
2539
|
+
* @param itemSelector Which descendants count as items (default: any `[data-key]`).
|
|
2540
|
+
*/
|
|
2541
|
+
constructor(root, itemSelector = `[data-key]`) {
|
|
2542
|
+
this.#root = root;
|
|
2543
|
+
this.#selector = itemSelector;
|
|
2544
|
+
this.#observer = new MutationObserver(() => {
|
|
2545
|
+
this.#dirty = true;
|
|
2546
|
+
});
|
|
2547
|
+
this.#observer.observe(root, {
|
|
2548
|
+
childList: true,
|
|
2549
|
+
subtree: true
|
|
2550
|
+
});
|
|
2551
|
+
}
|
|
2552
|
+
/** The element with this key, or `undefined`. First match wins on duplicates. */
|
|
2553
|
+
get(key) {
|
|
2554
|
+
if (this.#dirty) this.#rebuild();
|
|
2555
|
+
return this.#map.get(key);
|
|
2556
|
+
}
|
|
2557
|
+
/** Force a rebuild on the next `get()` — e.g. after a `data-key` attribute change. */
|
|
2558
|
+
invalidate() {
|
|
2559
|
+
this.#dirty = true;
|
|
2560
|
+
}
|
|
2561
|
+
/** Stop observing and drop the cache. */
|
|
2562
|
+
dispose() {
|
|
2563
|
+
this.#observer.disconnect();
|
|
2564
|
+
this.#map.clear();
|
|
2565
|
+
}
|
|
2566
|
+
#rebuild() {
|
|
2567
|
+
this.#map.clear();
|
|
2568
|
+
for (const el of this.#root.querySelectorAll(this.#selector)) {
|
|
2569
|
+
const key = keyForElement(el);
|
|
2570
|
+
if (key !== void 0 && !this.#map.has(key)) this.#map.set(key, el);
|
|
2571
|
+
}
|
|
2572
|
+
this.#dirty = false;
|
|
2573
|
+
}
|
|
2574
|
+
};
|
|
2575
|
+
//#endregion
|
|
2576
|
+
//#region src/incr-search/element-list-search-adapter.ts
|
|
2577
|
+
function defaultLabel(el) {
|
|
2578
|
+
return el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``;
|
|
2579
|
+
}
|
|
2580
|
+
/**
|
|
2581
|
+
* A {@link ListSearchAdapter} over the light-DOM item elements of a list
|
|
2582
|
+
* component. Filter mode toggles `hidden` (only on elements it hid itself, so a
|
|
2583
|
+
* consumer's own `hidden` items are left alone). Highlights are painted on the
|
|
2584
|
+
* item elements in the light DOM — the consuming page styles
|
|
2585
|
+
* `::highlight(<highlightKey>)`. See `docs/infra-incremental.md`.
|
|
2586
|
+
*/
|
|
2587
|
+
function createElementListSearchAdapter(host, options = {}) {
|
|
2588
|
+
const itemSelector = options.itemSelector ?? `:scope > *`;
|
|
2589
|
+
const getLabel = options.getLabel ?? defaultLabel;
|
|
2590
|
+
const getKey = options.getKey ?? ((el) => keyForElement(el) ?? defaultLabel(el));
|
|
2591
|
+
const hidden = /* @__PURE__ */ new Set();
|
|
2592
|
+
const items = () => Array.from(host.querySelectorAll(itemSelector));
|
|
2593
|
+
return {
|
|
2594
|
+
getItems: items,
|
|
2595
|
+
getLabel,
|
|
2596
|
+
getKey,
|
|
2597
|
+
focusItem: (el) => {
|
|
2598
|
+
if (el) host.focusKey(getKey(el));
|
|
2599
|
+
},
|
|
2600
|
+
activateItem: (el) => {
|
|
2601
|
+
el.dispatchEvent(new MouseEvent(`click`, {
|
|
2602
|
+
bubbles: true,
|
|
2603
|
+
composed: true
|
|
2604
|
+
}));
|
|
2605
|
+
},
|
|
2606
|
+
selectItems: (els) => {
|
|
2607
|
+
host.selectKeys(els.map(getKey));
|
|
2608
|
+
},
|
|
2609
|
+
applyFilter: (matched) => {
|
|
2610
|
+
if (matched === void 0) {
|
|
2611
|
+
for (const el of hidden) el.hidden = false;
|
|
2612
|
+
hidden.clear();
|
|
2613
|
+
return;
|
|
2614
|
+
}
|
|
2615
|
+
for (const el of items()) if (matched.has(getKey(el))) {
|
|
2616
|
+
if (hidden.delete(el)) el.hidden = false;
|
|
2617
|
+
} else {
|
|
2618
|
+
el.hidden = true;
|
|
2619
|
+
hidden.add(el);
|
|
2620
|
+
}
|
|
2621
|
+
},
|
|
2622
|
+
highlightRoot: () => host,
|
|
2623
|
+
highlightSelector: itemSelector,
|
|
2624
|
+
highlightKeyOf: (el) => getKey(el),
|
|
2625
|
+
highlightText: getLabel,
|
|
2626
|
+
highlightKey: options.highlightKey,
|
|
2627
|
+
container: () => host
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
//#endregion
|
|
2485
2631
|
//#region src/incr-search/element-search.ts
|
|
2486
2632
|
const defaultGetLabel = (el) => el.textContent?.trim() ?? ``;
|
|
2487
2633
|
function defaultGetKey(el) {
|
|
2488
2634
|
return el.getAttribute(`data-key`) ?? el.getAttribute(`data-value`) ?? el.textContent?.trim() ?? ``;
|
|
2489
2635
|
}
|
|
2636
|
+
/**
|
|
2637
|
+
* DOM filter-search over a container's item elements (sets `hidden` on
|
|
2638
|
+
* non-matches, applies highlights).
|
|
2639
|
+
*
|
|
2640
|
+
* @deprecated The list components (`ixfx-grid-list`, `ixfx-vertical-list`,
|
|
2641
|
+
* `ixfx-detail-list`, …) now have a built-in navigate / select / filter search
|
|
2642
|
+
* popover — see `docs/infra-incremental.md` and `ListSearchController`. Kept for
|
|
2643
|
+
* standalone / non-component use.
|
|
2644
|
+
*/
|
|
2490
2645
|
function createElementIncrSearch(opts) {
|
|
2491
2646
|
const { container, itemSelector, getLabel = defaultGetLabel, getKey = defaultGetKey, labelSelector, highlightKey } = opts;
|
|
2492
2647
|
let elementByKey = /* @__PURE__ */ new Map();
|
|
@@ -2560,6 +2715,512 @@ function createElementIncrSearch(opts) {
|
|
|
2560
2715
|
};
|
|
2561
2716
|
}
|
|
2562
2717
|
//#endregion
|
|
2718
|
+
//#region src/incr-search/list-search-commands.ts
|
|
2719
|
+
/** Command ids registered by {@link registerListSearchCommands}, keyed by mode. */
|
|
2720
|
+
const LIST_SEARCH_COMMAND_IDS = {
|
|
2721
|
+
navigate: `list.search.navigate`,
|
|
2722
|
+
select: `list.search.select`,
|
|
2723
|
+
filter: `list.search.filter`
|
|
2724
|
+
};
|
|
2725
|
+
const LABELS = {
|
|
2726
|
+
navigate: {
|
|
2727
|
+
label: `Find`,
|
|
2728
|
+
description: `Type to move the cursor to the best match`
|
|
2729
|
+
},
|
|
2730
|
+
select: {
|
|
2731
|
+
label: `Find & Select`,
|
|
2732
|
+
description: `Type to select every matching item`
|
|
2733
|
+
},
|
|
2734
|
+
filter: {
|
|
2735
|
+
label: `Filter`,
|
|
2736
|
+
description: `Type to hide items that don't match`
|
|
2737
|
+
}
|
|
2738
|
+
};
|
|
2739
|
+
/**
|
|
2740
|
+
* Register one command per mode the controller offers (see
|
|
2741
|
+
* `controller.registeredModes`). Each command opens the search popover in its
|
|
2742
|
+
* mode. Returns a function that unregisters them again — call it from
|
|
2743
|
+
* `disconnectedCallback`. See `docs/infra-incremental.md`.
|
|
2744
|
+
*/
|
|
2745
|
+
function registerListSearchCommands(registry, controller, options = {}) {
|
|
2746
|
+
const id = (mode) => options.idPrefix ? `${options.idPrefix}.search.${mode}` : LIST_SEARCH_COMMAND_IDS[mode];
|
|
2747
|
+
const registered = [];
|
|
2748
|
+
for (const mode of controller.registeredModes) {
|
|
2749
|
+
const commandId = id(mode);
|
|
2750
|
+
if (registry.has(commandId)) continue;
|
|
2751
|
+
registry.register({
|
|
2752
|
+
id: commandId,
|
|
2753
|
+
label: LABELS[mode].label,
|
|
2754
|
+
description: LABELS[mode].description,
|
|
2755
|
+
execute: () => controller.open(mode)
|
|
2756
|
+
});
|
|
2757
|
+
registered.push(commandId);
|
|
2758
|
+
}
|
|
2759
|
+
return () => {
|
|
2760
|
+
for (const commandId of registered) registry.unregister(commandId);
|
|
2761
|
+
};
|
|
2762
|
+
}
|
|
2763
|
+
//#endregion
|
|
2764
|
+
//#region src/incr-search/list-search-controller.ts
|
|
2765
|
+
/** Canonical order — used for the switch-row and to normalise a requested set. */
|
|
2766
|
+
const LIST_SEARCH_MODES = [
|
|
2767
|
+
`navigate`,
|
|
2768
|
+
`select`,
|
|
2769
|
+
`filter`
|
|
2770
|
+
];
|
|
2771
|
+
const DEFAULT_HIGHLIGHT_KEY = `incr-search-highlight`;
|
|
2772
|
+
const INPUT_CLASS = `list-search-input`;
|
|
2773
|
+
/**
|
|
2774
|
+
* Shared brain for the three-mode incremental-search popover. A Lit
|
|
2775
|
+
* `ReactiveController`: the host adds `listSearchStyles` to `static styles`,
|
|
2776
|
+
* drops `${controller.renderField()}` into `render()`, and registers the
|
|
2777
|
+
* commands with {@link registerListSearchCommands}. See `docs/infra-incremental.md`.
|
|
2778
|
+
*/
|
|
2779
|
+
var ListSearchController = class {
|
|
2780
|
+
#host;
|
|
2781
|
+
#adapter;
|
|
2782
|
+
#modes;
|
|
2783
|
+
#placeholder;
|
|
2784
|
+
#search;
|
|
2785
|
+
#unsub;
|
|
2786
|
+
#observer;
|
|
2787
|
+
#itemByKey = /* @__PURE__ */ new Map();
|
|
2788
|
+
#open = false;
|
|
2789
|
+
#mode = `filter`;
|
|
2790
|
+
#query = ``;
|
|
2791
|
+
#matchIndex = 0;
|
|
2792
|
+
#matches = [];
|
|
2793
|
+
/** Whether a filter has been pushed to the adapter and still needs undoing. */
|
|
2794
|
+
#filterActive = false;
|
|
2795
|
+
constructor(host, options) {
|
|
2796
|
+
this.#host = host;
|
|
2797
|
+
host.addController(this);
|
|
2798
|
+
this.#adapter = options.adapter;
|
|
2799
|
+
const requested = new Set(options.modes ?? LIST_SEARCH_MODES);
|
|
2800
|
+
this.#modes = LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
2801
|
+
this.#mode = this.#modes[0] ?? `filter`;
|
|
2802
|
+
this.#placeholder = options.placeholder ?? `Search…`;
|
|
2803
|
+
this.#search = createIncrSearch({
|
|
2804
|
+
selector: (i) => i.label,
|
|
2805
|
+
fzf: options.fzf
|
|
2806
|
+
});
|
|
2807
|
+
this.#unsub = this.#search.onChange((state) => this.#onState(state));
|
|
2808
|
+
}
|
|
2809
|
+
/** `false` when no modes are available — the host should register no commands and render no field. */
|
|
2810
|
+
get enabled() {
|
|
2811
|
+
return this.#modes.length > 0;
|
|
2812
|
+
}
|
|
2813
|
+
/** The modes actually offered (component capability ∩ consumer request). */
|
|
2814
|
+
get registeredModes() {
|
|
2815
|
+
return this.#modes;
|
|
2816
|
+
}
|
|
2817
|
+
/**
|
|
2818
|
+
* Replace the offered mode set (normalised to {@link LIST_SEARCH_MODES}
|
|
2819
|
+
* order). If the current mode is dropped the popover closes and falls back to
|
|
2820
|
+
* the first remaining mode. Callers should re-run
|
|
2821
|
+
* {@link registerListSearchCommands} afterwards.
|
|
2822
|
+
*/
|
|
2823
|
+
setModes(modes) {
|
|
2824
|
+
const set = new Set(modes);
|
|
2825
|
+
this.#modes = LIST_SEARCH_MODES.filter((m) => set.has(m));
|
|
2826
|
+
if (!this.#modes.includes(this.#mode)) {
|
|
2827
|
+
if (this.#open) this.close();
|
|
2828
|
+
this.#mode = this.#modes[0] ?? `filter`;
|
|
2829
|
+
}
|
|
2830
|
+
this.#host.requestUpdate();
|
|
2831
|
+
}
|
|
2832
|
+
get isOpen() {
|
|
2833
|
+
return this.#open;
|
|
2834
|
+
}
|
|
2835
|
+
get mode() {
|
|
2836
|
+
return this.#mode;
|
|
2837
|
+
}
|
|
2838
|
+
get query() {
|
|
2839
|
+
return this.#query;
|
|
2840
|
+
}
|
|
2841
|
+
get matchCount() {
|
|
2842
|
+
return this.#matches.length;
|
|
2843
|
+
}
|
|
2844
|
+
get matchIndex() {
|
|
2845
|
+
return this.#matchIndex;
|
|
2846
|
+
}
|
|
2847
|
+
hostConnected() {
|
|
2848
|
+
const container = this.#adapter.container?.();
|
|
2849
|
+
if (container) {
|
|
2850
|
+
this.#observer = new MutationObserver(() => this.refresh());
|
|
2851
|
+
this.#observer.observe(container, {
|
|
2852
|
+
childList: true,
|
|
2853
|
+
subtree: false
|
|
2854
|
+
});
|
|
2855
|
+
}
|
|
2856
|
+
this.refresh();
|
|
2857
|
+
}
|
|
2858
|
+
hostDisconnected() {
|
|
2859
|
+
this.#observer?.disconnect();
|
|
2860
|
+
this.#observer = void 0;
|
|
2861
|
+
if (this.#open) this.#teardownEffects();
|
|
2862
|
+
this.#open = false;
|
|
2863
|
+
this.#unsub?.();
|
|
2864
|
+
this.#unsub = void 0;
|
|
2865
|
+
this.#search.dispose();
|
|
2866
|
+
}
|
|
2867
|
+
/** Open the popover in `mode` (clamped to the available set). */
|
|
2868
|
+
open(mode) {
|
|
2869
|
+
if (!this.enabled) return;
|
|
2870
|
+
this.#mode = this.#modes.includes(mode) ? mode : this.#modes[0];
|
|
2871
|
+
this.#open = true;
|
|
2872
|
+
this.#query = ``;
|
|
2873
|
+
this.#matchIndex = 0;
|
|
2874
|
+
this.#matches = [];
|
|
2875
|
+
this.refresh();
|
|
2876
|
+
this.#search.setQuery(``);
|
|
2877
|
+
this.#host.requestUpdate();
|
|
2878
|
+
this.#host.updateComplete.then(() => this.#focusInput());
|
|
2879
|
+
}
|
|
2880
|
+
/** Close the popover, restoring any live filter. Selection / cursor stay put. */
|
|
2881
|
+
close() {
|
|
2882
|
+
if (!this.#open) return;
|
|
2883
|
+
this.#open = false;
|
|
2884
|
+
this.#teardownEffects();
|
|
2885
|
+
this.#query = ``;
|
|
2886
|
+
this.#matches = [];
|
|
2887
|
+
this.#search.clearQuery();
|
|
2888
|
+
this.#host.requestUpdate();
|
|
2889
|
+
this.#host.focus();
|
|
2890
|
+
}
|
|
2891
|
+
setQuery(query) {
|
|
2892
|
+
this.#query = query;
|
|
2893
|
+
this.#matchIndex = 0;
|
|
2894
|
+
this.#search.setQuery(query);
|
|
2895
|
+
}
|
|
2896
|
+
/** Re-interpret the current query under a new mode (undoing the old mode's effect). */
|
|
2897
|
+
switchMode(next) {
|
|
2898
|
+
if (!this.#modes.includes(next) || next === this.#mode) return;
|
|
2899
|
+
if (this.#mode === `filter` && this.#filterActive) {
|
|
2900
|
+
this.#adapter.applyFilter(void 0);
|
|
2901
|
+
this.#filterActive = false;
|
|
2902
|
+
}
|
|
2903
|
+
this.#mode = next;
|
|
2904
|
+
this.#applyMode();
|
|
2905
|
+
this.#host.requestUpdate();
|
|
2906
|
+
this.#host.updateComplete.then(() => this.#focusInput());
|
|
2907
|
+
}
|
|
2908
|
+
/** One-shot: turn the current match set into the selection, staying in the current mode. */
|
|
2909
|
+
selectMatches() {
|
|
2910
|
+
if (!this.#modes.includes(`select`)) return;
|
|
2911
|
+
this.#adapter.selectItems(this.#currentItems());
|
|
2912
|
+
this.#host.requestUpdate();
|
|
2913
|
+
}
|
|
2914
|
+
nextMatch() {
|
|
2915
|
+
this.#cycle(1);
|
|
2916
|
+
}
|
|
2917
|
+
prevMatch() {
|
|
2918
|
+
this.#cycle(-1);
|
|
2919
|
+
}
|
|
2920
|
+
/** Enter: in `navigate` activate the current match; then close. */
|
|
2921
|
+
commit() {
|
|
2922
|
+
if (this.#mode === `navigate`) {
|
|
2923
|
+
const item = this.#itemFor(this.#matches[this.#matchIndex]);
|
|
2924
|
+
if (item) {
|
|
2925
|
+
if (this.#adapter.activateItem) this.#adapter.activateItem(item);
|
|
2926
|
+
else this.#adapter.focusItem(item);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
this.close();
|
|
2930
|
+
}
|
|
2931
|
+
/** Rebuild the searchable item set from the adapter. Called on open and on container mutation. */
|
|
2932
|
+
refresh() {
|
|
2933
|
+
this.#itemByKey.clear();
|
|
2934
|
+
const items = [];
|
|
2935
|
+
for (const it of this.#adapter.getItems()) {
|
|
2936
|
+
const key = this.#adapter.getKey(it);
|
|
2937
|
+
this.#itemByKey.set(key, it);
|
|
2938
|
+
items.push({
|
|
2939
|
+
key,
|
|
2940
|
+
label: this.#adapter.getLabel(it)
|
|
2941
|
+
});
|
|
2942
|
+
}
|
|
2943
|
+
this.#search.setItems(items);
|
|
2944
|
+
}
|
|
2945
|
+
/** Popover markup. Pair with `listSearchStyles` in the host's `static styles`. */
|
|
2946
|
+
renderField() {
|
|
2947
|
+
if (!this.enabled) return A;
|
|
2948
|
+
const hasQuery = this.#query.trim().length > 0;
|
|
2949
|
+
const others = this.#modes.filter((m) => m !== this.#mode);
|
|
2950
|
+
const showSelectMatches = this.#modes.includes(`select`) && this.#mode !== `select`;
|
|
2951
|
+
return b`
|
|
2952
|
+
<div class="list-search" part="search" ?hidden=${!this.#open}>
|
|
2953
|
+
<div class="list-search-row">
|
|
2954
|
+
<span class="list-search-mode">${this.#mode}</span>
|
|
2955
|
+
<input
|
|
2956
|
+
class=${INPUT_CLASS}
|
|
2957
|
+
type="text"
|
|
2958
|
+
part="search-input"
|
|
2959
|
+
placeholder=${this.#placeholder}
|
|
2960
|
+
.value=${this.#query}
|
|
2961
|
+
@input=${this.#onInput}
|
|
2962
|
+
@keydown=${this.#onKeydown}
|
|
2963
|
+
/>
|
|
2964
|
+
${hasQuery ? b`<span class="list-search-count">${this.#matches.length === 0 ? `0` : this.#matchIndex + 1} / ${this.#matches.length}</span>` : A}
|
|
2965
|
+
${this.#mode === `navigate` && this.#matches.length > 1 ? b`
|
|
2966
|
+
<button class="list-search-icon-btn" title="Previous match (↑)" @click=${() => this.prevMatch()}>‹</button>
|
|
2967
|
+
<button class="list-search-icon-btn" title="Next match (↓)" @click=${() => this.nextMatch()}>›</button>
|
|
2968
|
+
` : A}
|
|
2969
|
+
<button class="list-search-icon-btn" title="Close (Esc)" @click=${() => this.close()}>✕</button>
|
|
2970
|
+
</div>
|
|
2971
|
+
${others.length > 0 || showSelectMatches ? b`
|
|
2972
|
+
<div class="list-search-switch">
|
|
2973
|
+
${others.map((m) => b`<button class="list-search-btn" @click=${() => this.switchMode(m)}>${m}</button>`)}
|
|
2974
|
+
${showSelectMatches ? b`<button class="list-search-btn" @click=${() => this.selectMatches()}>Select matches</button>` : A}
|
|
2975
|
+
</div>
|
|
2976
|
+
` : A}
|
|
2977
|
+
</div>
|
|
2978
|
+
`;
|
|
2979
|
+
}
|
|
2980
|
+
#focusInput() {
|
|
2981
|
+
this.#host.renderRoot?.querySelector(`.${INPUT_CLASS}`)?.focus();
|
|
2982
|
+
}
|
|
2983
|
+
#onInput = (e) => {
|
|
2984
|
+
this.setQuery(e.target.value);
|
|
2985
|
+
};
|
|
2986
|
+
#onKeydown = (e) => {
|
|
2987
|
+
switch (e.key) {
|
|
2988
|
+
case `Escape`:
|
|
2989
|
+
e.preventDefault();
|
|
2990
|
+
e.stopPropagation();
|
|
2991
|
+
this.close();
|
|
2992
|
+
break;
|
|
2993
|
+
case `Enter`:
|
|
2994
|
+
e.preventDefault();
|
|
2995
|
+
e.stopPropagation();
|
|
2996
|
+
this.commit();
|
|
2997
|
+
break;
|
|
2998
|
+
case `ArrowDown`:
|
|
2999
|
+
if (this.#mode === `navigate`) {
|
|
3000
|
+
e.preventDefault();
|
|
3001
|
+
this.nextMatch();
|
|
3002
|
+
}
|
|
3003
|
+
break;
|
|
3004
|
+
case `ArrowUp`: if (this.#mode === `navigate`) {
|
|
3005
|
+
e.preventDefault();
|
|
3006
|
+
this.prevMatch();
|
|
3007
|
+
}
|
|
3008
|
+
}
|
|
3009
|
+
};
|
|
3010
|
+
#cycle(delta) {
|
|
3011
|
+
if (this.#matches.length === 0) return;
|
|
3012
|
+
const n = this.#matches.length;
|
|
3013
|
+
this.#matchIndex = (this.#matchIndex + delta + n) % n;
|
|
3014
|
+
if (this.#mode === `navigate`) this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
|
|
3015
|
+
this.#host.requestUpdate();
|
|
3016
|
+
}
|
|
3017
|
+
#itemFor(match) {
|
|
3018
|
+
return match ? this.#itemByKey.get(match.item.key) : void 0;
|
|
3019
|
+
}
|
|
3020
|
+
#currentItems() {
|
|
3021
|
+
return this.#matches.map((m) => this.#itemFor(m)).filter((x) => x !== void 0);
|
|
3022
|
+
}
|
|
3023
|
+
get #highlightKey() {
|
|
3024
|
+
return this.#adapter.highlightKey ?? DEFAULT_HIGHLIGHT_KEY;
|
|
3025
|
+
}
|
|
3026
|
+
#onState(state) {
|
|
3027
|
+
if (!this.#open) return;
|
|
3028
|
+
const hasQuery = state.query.trim().length > 0;
|
|
3029
|
+
this.#matches = hasQuery ? [...state.results] : [];
|
|
3030
|
+
if (this.#matchIndex >= this.#matches.length) this.#matchIndex = 0;
|
|
3031
|
+
this.#applyMode();
|
|
3032
|
+
this.#host.requestUpdate();
|
|
3033
|
+
}
|
|
3034
|
+
#applyMode() {
|
|
3035
|
+
if (!(this.#query.trim().length > 0)) {
|
|
3036
|
+
if (this.#filterActive) {
|
|
3037
|
+
this.#adapter.applyFilter(void 0);
|
|
3038
|
+
this.#filterActive = false;
|
|
3039
|
+
}
|
|
3040
|
+
this.#applyHighlights();
|
|
3041
|
+
return;
|
|
3042
|
+
}
|
|
3043
|
+
switch (this.#mode) {
|
|
3044
|
+
case `navigate`:
|
|
3045
|
+
this.#adapter.focusItem(this.#itemFor(this.#matches[this.#matchIndex]));
|
|
3046
|
+
break;
|
|
3047
|
+
case `select`:
|
|
3048
|
+
this.#adapter.selectItems(this.#currentItems());
|
|
3049
|
+
break;
|
|
3050
|
+
case `filter`:
|
|
3051
|
+
this.#adapter.applyFilter(new Set(this.#matches.map((m) => m.item.key)));
|
|
3052
|
+
this.#filterActive = true;
|
|
3053
|
+
}
|
|
3054
|
+
this.#applyHighlights();
|
|
3055
|
+
}
|
|
3056
|
+
#applyHighlights() {
|
|
3057
|
+
const root = this.#adapter.highlightRoot?.();
|
|
3058
|
+
const selector = this.#adapter.highlightSelector;
|
|
3059
|
+
const keyOf = this.#adapter.highlightKeyOf;
|
|
3060
|
+
if (!root || !selector || !keyOf) return;
|
|
3061
|
+
if (this.#query.trim().length === 0) {
|
|
3062
|
+
clearHighlights({ key: this.#highlightKey });
|
|
3063
|
+
return;
|
|
3064
|
+
}
|
|
3065
|
+
const state = this.#search.state;
|
|
3066
|
+
const targets = buildHighlightTargets(root, selector, state.positionsByItem, (el) => {
|
|
3067
|
+
const key = keyOf(el);
|
|
3068
|
+
return key === void 0 ? void 0 : state.results.find((r) => r.item.key === key)?.item;
|
|
3069
|
+
}, this.#adapter.highlightText ?? ((el) => el.textContent ?? ``));
|
|
3070
|
+
try {
|
|
3071
|
+
applyHighlights(targets, { key: this.#highlightKey });
|
|
3072
|
+
} catch {
|
|
3073
|
+
clearHighlights({ key: this.#highlightKey });
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
#teardownEffects() {
|
|
3077
|
+
if (this.#filterActive) {
|
|
3078
|
+
this.#adapter.applyFilter(void 0);
|
|
3079
|
+
this.#filterActive = false;
|
|
3080
|
+
}
|
|
3081
|
+
clearHighlights({ key: this.#highlightKey });
|
|
3082
|
+
}
|
|
3083
|
+
};
|
|
3084
|
+
//#endregion
|
|
3085
|
+
//#region src/incr-search/list-search-styles.ts
|
|
3086
|
+
/**
|
|
3087
|
+
* Styles for the incremental-search popover rendered by
|
|
3088
|
+
* `ListSearchController.renderField()`. Add to a component's `static styles`;
|
|
3089
|
+
* the class names are the contract between this fragment and `renderField()`.
|
|
3090
|
+
*
|
|
3091
|
+
* The host must give the popover a positioned ancestor (a wrapper with
|
|
3092
|
+
* `position: relative`, or `:host { position: relative }`). Tunables:
|
|
3093
|
+
* `--list-search-inset` (default `8px`) offsets it from the top-right corner.
|
|
3094
|
+
*
|
|
3095
|
+
* See `docs/infra-incremental.md`.
|
|
3096
|
+
*/
|
|
3097
|
+
const listSearchStyles = i$3`
|
|
3098
|
+
.list-search {
|
|
3099
|
+
position: absolute;
|
|
3100
|
+
top: var(--list-search-inset, 8px);
|
|
3101
|
+
right: var(--list-search-inset, 8px);
|
|
3102
|
+
z-index: 20;
|
|
3103
|
+
display: flex;
|
|
3104
|
+
flex-direction: column;
|
|
3105
|
+
gap: 4px;
|
|
3106
|
+
padding: 6px;
|
|
3107
|
+
box-sizing: border-box;
|
|
3108
|
+
background: var(--surface-4, #2a2a2a);
|
|
3109
|
+
color: var(--surface-3-text, inherit);
|
|
3110
|
+
border: 1px solid var(--border, #444);
|
|
3111
|
+
border-radius: var(--radius-m, 6px);
|
|
3112
|
+
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
|
|
3113
|
+
font-size: var(--text-s, 0.85em);
|
|
3114
|
+
}
|
|
3115
|
+
|
|
3116
|
+
.list-search[hidden] {
|
|
3117
|
+
display: none;
|
|
3118
|
+
}
|
|
3119
|
+
|
|
3120
|
+
.list-search-row {
|
|
3121
|
+
display: flex;
|
|
3122
|
+
align-items: center;
|
|
3123
|
+
gap: 4px;
|
|
3124
|
+
}
|
|
3125
|
+
|
|
3126
|
+
.list-search-mode {
|
|
3127
|
+
flex: none;
|
|
3128
|
+
padding: 1px 7px;
|
|
3129
|
+
border-radius: 999px;
|
|
3130
|
+
font-weight: 600;
|
|
3131
|
+
text-transform: capitalize;
|
|
3132
|
+
background: var(--accent, #60a5fa);
|
|
3133
|
+
color: var(--accent-text, #000);
|
|
3134
|
+
}
|
|
3135
|
+
|
|
3136
|
+
.list-search-input {
|
|
3137
|
+
flex: 1;
|
|
3138
|
+
min-width: 140px;
|
|
3139
|
+
background: transparent;
|
|
3140
|
+
border: none;
|
|
3141
|
+
outline: none;
|
|
3142
|
+
color: inherit;
|
|
3143
|
+
font: inherit;
|
|
3144
|
+
}
|
|
3145
|
+
|
|
3146
|
+
.list-search-count {
|
|
3147
|
+
flex: none;
|
|
3148
|
+
opacity: 0.6;
|
|
3149
|
+
font-variant-numeric: tabular-nums;
|
|
3150
|
+
white-space: nowrap;
|
|
3151
|
+
}
|
|
3152
|
+
|
|
3153
|
+
.list-search-switch {
|
|
3154
|
+
display: flex;
|
|
3155
|
+
flex-wrap: wrap;
|
|
3156
|
+
gap: 4px;
|
|
3157
|
+
}
|
|
3158
|
+
|
|
3159
|
+
.list-search-btn {
|
|
3160
|
+
padding: 1px 7px;
|
|
3161
|
+
background: var(--surface-3, #333);
|
|
3162
|
+
color: inherit;
|
|
3163
|
+
border: 1px solid var(--border, #444);
|
|
3164
|
+
border-radius: var(--radius-s, 4px);
|
|
3165
|
+
cursor: pointer;
|
|
3166
|
+
font: inherit;
|
|
3167
|
+
text-transform: capitalize;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
.list-search-btn:hover {
|
|
3171
|
+
background: var(--item-bg-tickled, #444);
|
|
3172
|
+
}
|
|
3173
|
+
|
|
3174
|
+
.list-search-icon-btn {
|
|
3175
|
+
flex: none;
|
|
3176
|
+
padding: 0 2px;
|
|
3177
|
+
background: transparent;
|
|
3178
|
+
border: none;
|
|
3179
|
+
color: var(--surface-muted-text, #888);
|
|
3180
|
+
cursor: pointer;
|
|
3181
|
+
font-size: 1.1em;
|
|
3182
|
+
line-height: 1;
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
.list-search-icon-btn:hover {
|
|
3186
|
+
color: inherit;
|
|
3187
|
+
}
|
|
3188
|
+
`;
|
|
3189
|
+
//#endregion
|
|
3190
|
+
//#region src/incr-search/tree-list-search-adapter.ts
|
|
3191
|
+
/**
|
|
3192
|
+
* A {@link ListSearchAdapter} over any `TreeSearchHost` (`ixfx-tree-list`,
|
|
3193
|
+
* `ixfx-miller-list`, `ixfx-crumb-navigation`). Navigate reveals the match
|
|
3194
|
+
* (expands its ancestors, moves the cursor); select drives `selectMany`; filter
|
|
3195
|
+
* sets `filterPredicate`. The item set is the whole tree — `getNodes()` returns
|
|
3196
|
+
* every node regardless of expansion or an active filter — so refining a query
|
|
3197
|
+
* can always re-surface a node. See `docs/infra-incremental.md`.
|
|
3198
|
+
*/
|
|
3199
|
+
function createTreeListSearchAdapter(host, options = {}) {
|
|
3200
|
+
return {
|
|
3201
|
+
getItems: () => host.getNodes(),
|
|
3202
|
+
getLabel: options.getLabel ?? ((item) => item.node.item.label),
|
|
3203
|
+
getKey: (item) => item.node.item.key,
|
|
3204
|
+
focusItem: (item) => {
|
|
3205
|
+
if (item) host.revealKey(item.node.item.key);
|
|
3206
|
+
},
|
|
3207
|
+
activateItem: (item) => {
|
|
3208
|
+
host.revealKey(item.node.item.key);
|
|
3209
|
+
host.select(item.node);
|
|
3210
|
+
},
|
|
3211
|
+
selectItems: (items) => {
|
|
3212
|
+
host.selectMany(items.map((i) => i.node));
|
|
3213
|
+
},
|
|
3214
|
+
applyFilter: (matched) => {
|
|
3215
|
+
host.filterPredicate = matched === void 0 ? void 0 : (node) => matched.has(node.item.key);
|
|
3216
|
+
},
|
|
3217
|
+
highlightRoot: () => host.shadowRoot,
|
|
3218
|
+
highlightSelector: options.labelSelector ?? `.tree-label`,
|
|
3219
|
+
highlightKeyOf: (el) => el.closest(`[data-key]`)?.dataset.key,
|
|
3220
|
+
highlightKey: options.highlightKey
|
|
3221
|
+
};
|
|
3222
|
+
}
|
|
3223
|
+
//#endregion
|
|
2563
3224
|
//#region src/ac-token/ac-token.ts
|
|
2564
3225
|
const DEFAULT_TOKENIZER = (text) => text.split(/[\s,;]+/);
|
|
2565
3226
|
function makeLruCache(maxSize) {
|
|
@@ -3861,6 +4522,114 @@ __decorate([state()], RadioGroup.prototype, "_icons", void 0);
|
|
|
3861
4522
|
__decorate([query(`slot`)], RadioGroup.prototype, "_slot", void 0);
|
|
3862
4523
|
RadioGroup = __decorate([safeCustomElement(`ixfx-radio-group`)], RadioGroup);
|
|
3863
4524
|
//#endregion
|
|
4525
|
+
//#region src/interaction/list-item-animation.ts
|
|
4526
|
+
/**
|
|
4527
|
+
* Shared insertion / removal animation for the list & tree components.
|
|
4528
|
+
*
|
|
4529
|
+
* Two flavours, because the components split into two layout families:
|
|
4530
|
+
*
|
|
4531
|
+
* - **Flow layouts** (`ixfx-vertical-list`, `ixfx-detail-list`, `ixfx-tree-*`) —
|
|
4532
|
+
* rows sit in normal flow, so a new row can collapse in from zero height and a
|
|
4533
|
+
* leaving row can collapse back to zero, and the rows around it slide to make
|
|
4534
|
+
* or close the gap for free. Use {@link animateListItemIn} /
|
|
4535
|
+
* {@link animateListItemOut} (Web Animations API, exact measured sizes).
|
|
4536
|
+
*
|
|
4537
|
+
* - **Virtualised layouts** (`ixfx-grid-list`, `ixfx-data-grid`) — rows are
|
|
4538
|
+
* absolutely positioned (`transform` / `top`), so collapsing a row's box moves
|
|
4539
|
+
* nothing. Those components instead fade the entering / leaving row with the
|
|
4540
|
+
* {@link listItemFadeKeyframes} keyframes and slide the survivors by
|
|
4541
|
+
* transitioning their position for a short window after the change.
|
|
4542
|
+
*
|
|
4543
|
+
* Everything here is a no-op under `prefers-reduced-motion: reduce`.
|
|
4544
|
+
*/
|
|
4545
|
+
/** Default duration (ms) for both flavours; overridable per call / per component. */
|
|
4546
|
+
const LIST_ITEM_MOTION_MS = 200;
|
|
4547
|
+
function prefersReducedMotion() {
|
|
4548
|
+
return typeof window !== `undefined` && typeof window.matchMedia === `function` && window.matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
4549
|
+
}
|
|
4550
|
+
/**
|
|
4551
|
+
* Opacity keyframes for the virtualised components. Each one adds its own
|
|
4552
|
+
* one-line rule binding `.<row>.ixfx-item-entering` / `.ixfx-item-leaving` to
|
|
4553
|
+
* these, plus a `prefers-reduced-motion` guard on its own selectors.
|
|
4554
|
+
*/
|
|
4555
|
+
const listItemFadeKeyframes = i$3`
|
|
4556
|
+
@keyframes ixfx-list-item-in {
|
|
4557
|
+
from { opacity: 0; }
|
|
4558
|
+
to { opacity: 1; }
|
|
4559
|
+
}
|
|
4560
|
+
@keyframes ixfx-list-item-out {
|
|
4561
|
+
from { opacity: 1; }
|
|
4562
|
+
to { opacity: 0; }
|
|
4563
|
+
}
|
|
4564
|
+
`;
|
|
4565
|
+
function measure(el, axis) {
|
|
4566
|
+
const rect = el.getBoundingClientRect();
|
|
4567
|
+
return axis === `inline` ? rect.width : rect.height;
|
|
4568
|
+
}
|
|
4569
|
+
function resolveDuration(el, explicit) {
|
|
4570
|
+
if (explicit !== void 0) return explicit;
|
|
4571
|
+
const raw = getComputedStyle(el).getPropertyValue(`--ixfx-list-item-move-duration`).trim();
|
|
4572
|
+
if (raw) {
|
|
4573
|
+
const n = Number.parseFloat(raw);
|
|
4574
|
+
if (!Number.isNaN(n)) return raw.endsWith(`ms`) ? n : raw.endsWith(`s`) ? n * 1e3 : n;
|
|
4575
|
+
}
|
|
4576
|
+
return 200;
|
|
4577
|
+
}
|
|
4578
|
+
function collapsedFrame(axis) {
|
|
4579
|
+
const prop = axis === `inline` ? `width` : `height`;
|
|
4580
|
+
const minProp = axis === `inline` ? `minWidth` : `minHeight`;
|
|
4581
|
+
return {
|
|
4582
|
+
[prop]: `0px`,
|
|
4583
|
+
[minProp]: `0px`,
|
|
4584
|
+
opacity: 0,
|
|
4585
|
+
overflow: `hidden`
|
|
4586
|
+
};
|
|
4587
|
+
}
|
|
4588
|
+
function fullFrame(size, axis) {
|
|
4589
|
+
const prop = axis === `inline` ? `width` : `height`;
|
|
4590
|
+
const minProp = axis === `inline` ? `minWidth` : `minHeight`;
|
|
4591
|
+
return {
|
|
4592
|
+
[prop]: `${size}px`,
|
|
4593
|
+
[minProp]: `0px`,
|
|
4594
|
+
opacity: 1,
|
|
4595
|
+
overflow: `hidden`
|
|
4596
|
+
};
|
|
4597
|
+
}
|
|
4598
|
+
/**
|
|
4599
|
+
* Collapse + fade `el` in from nothing to its natural size. Call it just after
|
|
4600
|
+
* the element has been inserted (it must already be laid out at full size so it
|
|
4601
|
+
* can be measured). No-op under reduced motion.
|
|
4602
|
+
*/
|
|
4603
|
+
function animateListItemIn(el, options = {}) {
|
|
4604
|
+
if (prefersReducedMotion()) return void 0;
|
|
4605
|
+
const { axis = `block`, easing = `ease` } = options;
|
|
4606
|
+
const duration = resolveDuration(el, options.duration);
|
|
4607
|
+
const size = measure(el, axis);
|
|
4608
|
+
if (size <= 0) return void 0;
|
|
4609
|
+
return el.animate([collapsedFrame(axis), fullFrame(size, axis)], {
|
|
4610
|
+
duration,
|
|
4611
|
+
easing
|
|
4612
|
+
});
|
|
4613
|
+
}
|
|
4614
|
+
/**
|
|
4615
|
+
* Collapse + fade `el` out. Resolves once the animation finishes (or straight
|
|
4616
|
+
* away under reduced motion); the caller detaches the element after. The final
|
|
4617
|
+
* collapsed state is held (`fill: forwards`) so there's no flash back to full
|
|
4618
|
+
* size in the gap before removal.
|
|
4619
|
+
*/
|
|
4620
|
+
function animateListItemOut(el, options = {}) {
|
|
4621
|
+
if (prefersReducedMotion()) return Promise.resolve();
|
|
4622
|
+
const { axis = `block`, easing = `ease` } = options;
|
|
4623
|
+
const duration = resolveDuration(el, options.duration);
|
|
4624
|
+
const size = measure(el, axis);
|
|
4625
|
+
if (size <= 0) return Promise.resolve();
|
|
4626
|
+
return el.animate([fullFrame(size, axis), collapsedFrame(axis)], {
|
|
4627
|
+
duration,
|
|
4628
|
+
easing,
|
|
4629
|
+
fill: `forwards`
|
|
4630
|
+
}).finished.then(() => void 0, () => void 0);
|
|
4631
|
+
}
|
|
4632
|
+
//#endregion
|
|
3864
4633
|
//#region src/interaction/list-selection-controller.ts
|
|
3865
4634
|
/** Whether the interaction mode allows building a multi-item selection. */
|
|
3866
4635
|
function isMultiMode(mode) {
|
|
@@ -3901,6 +4670,20 @@ var ListSelectionController = class {
|
|
|
3901
4670
|
get selected() {
|
|
3902
4671
|
return this.#selected;
|
|
3903
4672
|
}
|
|
4673
|
+
/**
|
|
4674
|
+
* The current selection as stable string keys (read-only). Empty unless a
|
|
4675
|
+
* `getKey` was supplied in config. Items whose `getKey` returns `undefined`
|
|
4676
|
+
* are omitted.
|
|
4677
|
+
*/
|
|
4678
|
+
get selectedKeys() {
|
|
4679
|
+
const getKey = this.#config.getKey;
|
|
4680
|
+
const out = /* @__PURE__ */ new Set();
|
|
4681
|
+
if (getKey) for (const item of this.#selected) {
|
|
4682
|
+
const key = getKey(item);
|
|
4683
|
+
if (key !== void 0) out.add(key);
|
|
4684
|
+
}
|
|
4685
|
+
return out;
|
|
4686
|
+
}
|
|
3904
4687
|
/** Anchor for range (shift) selection — the last plainly-selected item. */
|
|
3905
4688
|
get anchor() {
|
|
3906
4689
|
return this.#anchor;
|
|
@@ -3984,6 +4767,66 @@ var ListSelectionController = class {
|
|
|
3984
4767
|
next.delete(item);
|
|
3985
4768
|
this.#apply(next);
|
|
3986
4769
|
}
|
|
4770
|
+
/**
|
|
4771
|
+
* Whether the current modes permit a selection of more than one item.
|
|
4772
|
+
* Stricter than `canSelectMultiple()`: `standard` / `implicit` only count
|
|
4773
|
+
* as multi when `selectionMode="multiple"`.
|
|
4774
|
+
*/
|
|
4775
|
+
#effectiveMulti() {
|
|
4776
|
+
const { selectionMode, interactionMode } = this.#config.getModes();
|
|
4777
|
+
return selectionMode === `multiple` || interactionMode === `checked` || interactionMode === `sticky` || interactionMode === `vscode`;
|
|
4778
|
+
}
|
|
4779
|
+
/**
|
|
4780
|
+
* Replace the selection with the items resolved from `keys`. Keys that
|
|
4781
|
+
* `resolve` cannot map, or that resolve to a non-selectable item, are
|
|
4782
|
+
* ignored. `selectionMode="none"` is a no-op. When multi-select is not
|
|
4783
|
+
* available, only the last resolvable key is kept. `onChange` fires once.
|
|
4784
|
+
*/
|
|
4785
|
+
selectKeys(keys, resolve) {
|
|
4786
|
+
if (this.#config.getModes().selectionMode === `none`) return;
|
|
4787
|
+
let next = /* @__PURE__ */ new Set();
|
|
4788
|
+
for (const key of new Set(keys)) {
|
|
4789
|
+
const item = resolve(key);
|
|
4790
|
+
if (item !== void 0 && this.#config.isSelectable(item)) next.add(item);
|
|
4791
|
+
}
|
|
4792
|
+
if (!this.#effectiveMulti() && next.size > 1) {
|
|
4793
|
+
const last = [...next].at(-1);
|
|
4794
|
+
next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
|
|
4795
|
+
}
|
|
4796
|
+
this.#anchor = [...next].at(-1);
|
|
4797
|
+
this.#apply(next);
|
|
4798
|
+
}
|
|
4799
|
+
/**
|
|
4800
|
+
* Add the items resolved from `keys` to the selection (multi-select only;
|
|
4801
|
+
* a no-op otherwise). `onChange` fires once if anything changed.
|
|
4802
|
+
*/
|
|
4803
|
+
addKeys(keys, resolve) {
|
|
4804
|
+
if (!this.#effectiveMulti()) return;
|
|
4805
|
+
const next = new Set(this.#selected);
|
|
4806
|
+
let changed = false;
|
|
4807
|
+
for (const key of new Set(keys)) {
|
|
4808
|
+
const item = resolve(key);
|
|
4809
|
+
if (item !== void 0 && !next.has(item) && this.#config.isSelectable(item)) {
|
|
4810
|
+
next.add(item);
|
|
4811
|
+
changed = true;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
if (changed) this.#apply(next);
|
|
4815
|
+
}
|
|
4816
|
+
/**
|
|
4817
|
+
* Remove the items resolved from `keys` from the selection. `onChange`
|
|
4818
|
+
* fires once if anything changed.
|
|
4819
|
+
*/
|
|
4820
|
+
removeKeys(keys, resolve) {
|
|
4821
|
+
if (this.#selected.size === 0) return;
|
|
4822
|
+
const next = new Set(this.#selected);
|
|
4823
|
+
let changed = false;
|
|
4824
|
+
for (const key of new Set(keys)) {
|
|
4825
|
+
const item = resolve(key);
|
|
4826
|
+
if (item !== void 0 && next.delete(item)) changed = true;
|
|
4827
|
+
}
|
|
4828
|
+
if (changed) this.#apply(next);
|
|
4829
|
+
}
|
|
3987
4830
|
/** Route a click on `item` according to the current interaction mode. */
|
|
3988
4831
|
handleClick(item, gesture) {
|
|
3989
4832
|
const { selectionMode, interactionMode: mode } = this.#config.getModes();
|
|
@@ -8285,6 +9128,16 @@ let CrumbNavigationElement = class CrumbNavigationElement extends i {
|
|
|
8285
9128
|
if (!keyPath) return false;
|
|
8286
9129
|
return this.navigateTo(keyPath);
|
|
8287
9130
|
}
|
|
9131
|
+
/** `TreeComponent` parity — a crumb path always follows the selection, so this just navigates to `key`. */
|
|
9132
|
+
revealKey(key) {
|
|
9133
|
+
return this.navigateToNode((n) => n.item.key === key);
|
|
9134
|
+
}
|
|
9135
|
+
/** `TreeComponent` parity — a crumb path is a single selection, so only the last node is kept. */
|
|
9136
|
+
selectMany(nodes) {
|
|
9137
|
+
const last = [...nodes].at(-1);
|
|
9138
|
+
if (last) this.select(last);
|
|
9139
|
+
else this.clearSelection();
|
|
9140
|
+
}
|
|
8288
9141
|
#findNodePath(nodes, predicate, prefix = []) {
|
|
8289
9142
|
for (const node of nodes) {
|
|
8290
9143
|
const path = [...prefix, node.item.key];
|
|
@@ -10772,6 +11625,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
10772
11625
|
this.groupBy = null;
|
|
10773
11626
|
this.allowGrouping = false;
|
|
10774
11627
|
this.crossGroupSelection = false;
|
|
11628
|
+
this.columnMenu = `auto`;
|
|
10775
11629
|
this._columnFormatters = /* @__PURE__ */ new Map();
|
|
10776
11630
|
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
10777
11631
|
this._scrollTop = 0;
|
|
@@ -10794,10 +11648,18 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
10794
11648
|
this._sortedRows = [];
|
|
10795
11649
|
this._flat = [];
|
|
10796
11650
|
this._flatKeys = [];
|
|
11651
|
+
this.#searchKeyByRow = /* @__PURE__ */ new Map();
|
|
11652
|
+
this.search = new ListSearchController(this, { adapter: this.#createSearchAdapter() });
|
|
11653
|
+
this.#prevFlatKeys = [];
|
|
11654
|
+
this.#enteringRowKeys = /* @__PURE__ */ new Set();
|
|
11655
|
+
this.#rowReflowTimer = 0;
|
|
11656
|
+
this.#dataGridRenderedOnce = false;
|
|
10797
11657
|
this._visibleColumns = [];
|
|
10798
11658
|
this._colOffsets = [];
|
|
10799
11659
|
this._colWidthSum = 0;
|
|
10800
11660
|
this._raf = 0;
|
|
11661
|
+
this.#appendInProgress = false;
|
|
11662
|
+
this.#resortRaf = 0;
|
|
10801
11663
|
this._drag = null;
|
|
10802
11664
|
this._onFocusOut = (e) => {
|
|
10803
11665
|
if (this._suppressBlurCommit) return;
|
|
@@ -10992,6 +11854,8 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
10992
11854
|
themeFallbacks,
|
|
10993
11855
|
tickledStyles,
|
|
10994
11856
|
tickledItemStyles,
|
|
11857
|
+
listItemFadeKeyframes,
|
|
11858
|
+
listSearchStyles,
|
|
10995
11859
|
i$3`
|
|
10996
11860
|
:host {
|
|
10997
11861
|
--data-grid-row-height: 28px;
|
|
@@ -11000,6 +11864,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11000
11864
|
--data-grid-header-bg: var(--surface-2);
|
|
11001
11865
|
--data-grid-border: var(--border);
|
|
11002
11866
|
--data-grid-header-text: var(--surface-3-text);
|
|
11867
|
+
position: relative;
|
|
11003
11868
|
display: block;
|
|
11004
11869
|
overflow: hidden;
|
|
11005
11870
|
border: var(--data-grid-border);
|
|
@@ -11048,6 +11913,22 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11048
11913
|
.row.item.tickled { background: var(--item-bg-tickled); border: var(--item-border-tickled); color: var(--item-text-tickled); }
|
|
11049
11914
|
.row.item.selected { background: var(--item-bg-selected); border: var(--item-border-selected); color: var(--item-text-selected); }
|
|
11050
11915
|
.row.item.selected.tickled { background: var(--item-bg-selected-tickled); }
|
|
11916
|
+
|
|
11917
|
+
/* Insertion / removal animation. Only while [reflowing] — the ~0.3s after
|
|
11918
|
+
the row set changes — do rows slide (their top transitions) to make or
|
|
11919
|
+
close a gap; a freshly inserted row also fades in. Kept scoped so it
|
|
11920
|
+
never fires while scrolling the virtualised list. */
|
|
11921
|
+
:host([reflowing]) .row.item,
|
|
11922
|
+
:host([reflowing]) .group-row {
|
|
11923
|
+
transition: top var(--data-grid-row-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1);
|
|
11924
|
+
}
|
|
11925
|
+
.row.item.entering {
|
|
11926
|
+
animation: ixfx-list-item-in var(--data-grid-row-move-duration, 200ms) ease both;
|
|
11927
|
+
}
|
|
11928
|
+
@media (prefers-reduced-motion: reduce) {
|
|
11929
|
+
:host([reflowing]) .row.item, :host([reflowing]) .group-row { transition: none; }
|
|
11930
|
+
.row.item.entering { animation: none; }
|
|
11931
|
+
}
|
|
11051
11932
|
.cell {
|
|
11052
11933
|
flex-shrink: 0; display: flex; align-items: center; padding: 0 var(--space-s);
|
|
11053
11934
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; border-right: var(--border-subtle);
|
|
@@ -11077,17 +11958,89 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11077
11958
|
`
|
|
11078
11959
|
];
|
|
11079
11960
|
}
|
|
11961
|
+
#searchRegistry;
|
|
11962
|
+
#unregisterSearchCommands;
|
|
11963
|
+
#searchKeyByRow;
|
|
11964
|
+
/** Command registry carrying this grid's `list.search.*` commands. */
|
|
11965
|
+
get registry() {
|
|
11966
|
+
return this.#searchRegistry;
|
|
11967
|
+
}
|
|
11968
|
+
#rowSearchText(row) {
|
|
11969
|
+
if (this.rowSearchText) return this.rowSearchText(row);
|
|
11970
|
+
const rec = row;
|
|
11971
|
+
return this._visibleColumns.map((c) => String(rec[c.key] ?? ``)).join(` `);
|
|
11972
|
+
}
|
|
11973
|
+
#createSearchAdapter() {
|
|
11974
|
+
return {
|
|
11975
|
+
getItems: () => {
|
|
11976
|
+
this.#searchKeyByRow.clear();
|
|
11977
|
+
this._sortedRows.forEach((r, i) => this.#searchKeyByRow.set(r, this.getRowKey(r, i)));
|
|
11978
|
+
return this._sortedRows;
|
|
11979
|
+
},
|
|
11980
|
+
getLabel: (row) => this.#rowSearchText(row),
|
|
11981
|
+
getKey: (row) => this.#searchKeyByRow.get(row) ?? String(this._sortedRows.indexOf(row)),
|
|
11982
|
+
focusItem: (row) => {
|
|
11983
|
+
if (row === void 0) return;
|
|
11984
|
+
const key = this.#searchKeyByRow.get(row);
|
|
11985
|
+
if (key !== void 0) this.tickleRow(key);
|
|
11986
|
+
},
|
|
11987
|
+
selectItems: (rows) => {
|
|
11988
|
+
this.selectKeys(rows.map((r) => this.#searchKeyByRow.get(r)).filter((k) => k !== void 0));
|
|
11989
|
+
},
|
|
11990
|
+
applyFilter: (matched) => {
|
|
11991
|
+
this.filterPredicate = matched === void 0 ? void 0 : (row) => {
|
|
11992
|
+
const k = this.#searchKeyByRow.get(row);
|
|
11993
|
+
return k === void 0 || matched.has(k);
|
|
11994
|
+
};
|
|
11995
|
+
}
|
|
11996
|
+
};
|
|
11997
|
+
}
|
|
11998
|
+
#effectiveSearchModes() {
|
|
11999
|
+
const raw = this.searchModes?.trim();
|
|
12000
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
12001
|
+
const requested = new Set(raw.split(/\s+/));
|
|
12002
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
12003
|
+
}
|
|
12004
|
+
#syncSearchCommands() {
|
|
12005
|
+
if (!this.#searchRegistry) return;
|
|
12006
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
12007
|
+
this.#unregisterSearchCommands?.();
|
|
12008
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
12009
|
+
}
|
|
12010
|
+
/** Row-key snapshot from the previous update, for the insertion / removal animation. */
|
|
12011
|
+
#prevFlatKeys;
|
|
12012
|
+
#enteringRowKeys;
|
|
12013
|
+
#rowReflowTimer;
|
|
12014
|
+
#dataGridRenderedOnce;
|
|
12015
|
+
#appendInProgress;
|
|
12016
|
+
#resortRaf;
|
|
11080
12017
|
connectedCallback() {
|
|
11081
12018
|
super.connectedCallback();
|
|
11082
12019
|
if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
|
|
11083
12020
|
this.addEventListener(`keydown`, this._onKeyDown);
|
|
11084
12021
|
this.addEventListener(`focusout`, this._onFocusOut);
|
|
12022
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
12023
|
+
this.#syncSearchCommands();
|
|
11085
12024
|
}
|
|
11086
12025
|
disconnectedCallback() {
|
|
11087
12026
|
super.disconnectedCallback();
|
|
11088
12027
|
this.removeEventListener(`keydown`, this._onKeyDown);
|
|
11089
12028
|
this.removeEventListener(`focusout`, this._onFocusOut);
|
|
11090
12029
|
cancelAnimationFrame(this._raf);
|
|
12030
|
+
this.#unregisterSearchCommands?.();
|
|
12031
|
+
this.#unregisterSearchCommands = void 0;
|
|
12032
|
+
this.#searchRegistry = void 0;
|
|
12033
|
+
if (this.#rowReflowTimer) {
|
|
12034
|
+
clearTimeout(this.#rowReflowTimer);
|
|
12035
|
+
this.#rowReflowTimer = 0;
|
|
12036
|
+
}
|
|
12037
|
+
}
|
|
12038
|
+
/**
|
|
12039
|
+
* Dispatch a command to this grid — routes through the command registry so
|
|
12040
|
+
* `list.search.*` (and any other registered commands) work.
|
|
12041
|
+
*/
|
|
12042
|
+
dispatch(commandId, args) {
|
|
12043
|
+
this.#searchRegistry?.invoke(commandId, args);
|
|
11091
12044
|
}
|
|
11092
12045
|
firstUpdated() {
|
|
11093
12046
|
new ResizeObserver(() => {
|
|
@@ -11114,8 +12067,11 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11114
12067
|
willUpdate(changed) {
|
|
11115
12068
|
super.willUpdate(changed);
|
|
11116
12069
|
this.setAttribute(`data-input-mode`, this._tickled.inputMode);
|
|
11117
|
-
if (changed.has(`
|
|
11118
|
-
this
|
|
12070
|
+
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
12071
|
+
if (!this.#appendInProgress) {
|
|
12072
|
+
if (changed.has(`rows`) || changed.has(`sortState`) || changed.has(`columns`) || changed.has(`groupBy`) || changed.has(`groupLabel`)) this._sortedRows = this._applySort([...this.rows]);
|
|
12073
|
+
this._rebuildFlat();
|
|
12074
|
+
}
|
|
11119
12075
|
this._visibleColumns = this.columns.filter((c) => c.visible !== false);
|
|
11120
12076
|
let off = 0;
|
|
11121
12077
|
this._colOffsets = this._visibleColumns.map((c) => {
|
|
@@ -11125,6 +12081,40 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11125
12081
|
});
|
|
11126
12082
|
this._colWidthSum = off;
|
|
11127
12083
|
if (this.interactionMode === `checked`) this._colWidthSum += 28;
|
|
12084
|
+
this.#noteFlatMutation();
|
|
12085
|
+
}
|
|
12086
|
+
/**
|
|
12087
|
+
* Diff the row-key list against the previous update. Rows that appeared get an
|
|
12088
|
+
* enter fade; any add/remove opens a short `[reflowing]` window so the rows
|
|
12089
|
+
* that shift slide (their `top` transitions) instead of snapping. Bounded in
|
|
12090
|
+
* time so it never bleeds into scroll virtualisation.
|
|
12091
|
+
*/
|
|
12092
|
+
#noteFlatMutation() {
|
|
12093
|
+
const now = this._flatKeys;
|
|
12094
|
+
const prev = this.#prevFlatKeys;
|
|
12095
|
+
this.#prevFlatKeys = now.slice();
|
|
12096
|
+
if (!this.#dataGridRenderedOnce) {
|
|
12097
|
+
this.#dataGridRenderedOnce = true;
|
|
12098
|
+
return;
|
|
12099
|
+
}
|
|
12100
|
+
if (prev.length === 0 || now.length === 0) {
|
|
12101
|
+
this.#enteringRowKeys.clear();
|
|
12102
|
+
return;
|
|
12103
|
+
}
|
|
12104
|
+
const before = new Set(prev);
|
|
12105
|
+
const after = new Set(now);
|
|
12106
|
+
const added = now.filter((k) => !before.has(k));
|
|
12107
|
+
const removed = prev.filter((k) => !after.has(k));
|
|
12108
|
+
if (added.length === now.length || added.length === 0 && removed.length === 0) return;
|
|
12109
|
+
this.#enteringRowKeys = new Set(added);
|
|
12110
|
+
this.toggleAttribute(`reflowing`, true);
|
|
12111
|
+
if (this.#rowReflowTimer) clearTimeout(this.#rowReflowTimer);
|
|
12112
|
+
this.#rowReflowTimer = window.setTimeout(() => {
|
|
12113
|
+
this.#rowReflowTimer = 0;
|
|
12114
|
+
this.#enteringRowKeys.clear();
|
|
12115
|
+
this.toggleAttribute(`reflowing`, false);
|
|
12116
|
+
this.requestUpdate();
|
|
12117
|
+
}, 320);
|
|
11128
12118
|
}
|
|
11129
12119
|
_getGroupKey(row) {
|
|
11130
12120
|
if (!this.groupBy) return null;
|
|
@@ -11132,18 +12122,23 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11132
12122
|
return String(this.groupBy(row));
|
|
11133
12123
|
}
|
|
11134
12124
|
_rebuildFlat() {
|
|
12125
|
+
const filtered = this.filterPredicate !== void 0;
|
|
12126
|
+
const source = filtered ? this._sortedRows.filter((r) => this.filterPredicate(r)) : this._sortedRows;
|
|
11135
12127
|
if (!this.groupBy) {
|
|
11136
|
-
this._flat =
|
|
11137
|
-
|
|
11138
|
-
|
|
11139
|
-
|
|
11140
|
-
|
|
11141
|
-
|
|
12128
|
+
this._flat = source.map((r, i) => {
|
|
12129
|
+
const idx = filtered ? this._sortedRows.indexOf(r) : i;
|
|
12130
|
+
return {
|
|
12131
|
+
type: `row`,
|
|
12132
|
+
key: this.getRowKey(r, idx),
|
|
12133
|
+
row: r,
|
|
12134
|
+
idx
|
|
12135
|
+
};
|
|
12136
|
+
});
|
|
11142
12137
|
this._flatKeys = this._flat.map((f) => f.key);
|
|
11143
12138
|
return;
|
|
11144
12139
|
}
|
|
11145
12140
|
const groups = /* @__PURE__ */ new Map();
|
|
11146
|
-
for (const r of
|
|
12141
|
+
for (const r of source) {
|
|
11147
12142
|
const k = this._getGroupKey(r) ?? ``;
|
|
11148
12143
|
const arr = groups.get(k);
|
|
11149
12144
|
if (arr) arr.push(r);
|
|
@@ -11372,6 +12367,102 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11372
12367
|
_getRowForKey(key) {
|
|
11373
12368
|
return this._flat.find((f) => f.key === key)?.row;
|
|
11374
12369
|
}
|
|
12370
|
+
/** The selected row keys (read-only). */
|
|
12371
|
+
get selectedKeys() {
|
|
12372
|
+
return this._selectedKeys;
|
|
12373
|
+
}
|
|
12374
|
+
/** Replace the selection with these row keys (clamped to known rows). */
|
|
12375
|
+
set selectedKeys(keys) {
|
|
12376
|
+
this.selectKeys(keys);
|
|
12377
|
+
}
|
|
12378
|
+
/**
|
|
12379
|
+
* Replace the selection with the given row keys. Unknown keys are dropped;
|
|
12380
|
+
* in a single-select mode only the last surviving key is kept;
|
|
12381
|
+
* `selectionMode="none"` clears. Fires `datagrid-select` with the previous
|
|
12382
|
+
* set when the selection actually changes.
|
|
12383
|
+
*/
|
|
12384
|
+
selectKeys(keys) {
|
|
12385
|
+
const known = new Set(this._flat.filter((f) => f.type === `row`).map((f) => f.key));
|
|
12386
|
+
let next = new Set([...keys].filter((k) => known.has(k)));
|
|
12387
|
+
const multi = this.selectionMode === `multiple` || this.interactionMode === `checked` || this.interactionMode === `sticky` || this.interactionMode === `vscode`;
|
|
12388
|
+
if (this.selectionMode === `none`) next = /* @__PURE__ */ new Set();
|
|
12389
|
+
else if (!multi && next.size > 1) {
|
|
12390
|
+
const last = [...next].at(-1);
|
|
12391
|
+
next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
|
|
12392
|
+
}
|
|
12393
|
+
const prev = new Set(this._selectedKeys);
|
|
12394
|
+
if (prev.size === next.size && [...next].every((k) => prev.has(k))) return;
|
|
12395
|
+
this._selectedKeys = next;
|
|
12396
|
+
this._anchorKey = [...next].at(-1);
|
|
12397
|
+
this.dispatchEvent(new CustomEvent(`datagrid-select`, {
|
|
12398
|
+
detail: {
|
|
12399
|
+
selectedKeys: new Set(next),
|
|
12400
|
+
previous: prev
|
|
12401
|
+
},
|
|
12402
|
+
bubbles: true,
|
|
12403
|
+
composed: true
|
|
12404
|
+
}));
|
|
12405
|
+
}
|
|
12406
|
+
/** The row key under the cursor, or `undefined`. */
|
|
12407
|
+
get tickledKey() {
|
|
12408
|
+
return this._tickled.tickledId;
|
|
12409
|
+
}
|
|
12410
|
+
/** Move the cursor to this row key and scroll it into view. */
|
|
12411
|
+
tickleRow(key) {
|
|
12412
|
+
if (!this._flatKeys.includes(key)) return;
|
|
12413
|
+
this._tickled.focus(key);
|
|
12414
|
+
this._scrollRowIntoView(key);
|
|
12415
|
+
}
|
|
12416
|
+
/**
|
|
12417
|
+
* Append rows without the full O(n log n) rebuild that reassigning `rows`
|
|
12418
|
+
* incurs. `rows` still gets a new array reference (reactivity is preserved),
|
|
12419
|
+
* but the derived arrays are extended over the appended slice only. When a
|
|
12420
|
+
* sort is active the new rows show unsorted for one frame, then a single
|
|
12421
|
+
* coalesced re-sort runs. Scroll position, selection and cursor are kept.
|
|
12422
|
+
*/
|
|
12423
|
+
appendRows(rows) {
|
|
12424
|
+
if (rows.length === 0) return;
|
|
12425
|
+
const startIdx = this.rows.length;
|
|
12426
|
+
this.#appendInProgress = true;
|
|
12427
|
+
this.rows = [...this.rows, ...rows];
|
|
12428
|
+
const appended = this.rows.slice(startIdx);
|
|
12429
|
+
if (this._sortedRows.length === startIdx) this._sortedRows = [...this._sortedRows, ...appended];
|
|
12430
|
+
else {
|
|
12431
|
+
this._sortedRows = this._applySort([...this.rows]);
|
|
12432
|
+
this._rebuildFlat();
|
|
12433
|
+
this.#appendInProgress = false;
|
|
12434
|
+
this.requestUpdate();
|
|
12435
|
+
return;
|
|
12436
|
+
}
|
|
12437
|
+
this.#appendToFlat(appended, startIdx);
|
|
12438
|
+
this.#appendInProgress = false;
|
|
12439
|
+
this.requestUpdate();
|
|
12440
|
+
if (this.sortState != null) {
|
|
12441
|
+
cancelAnimationFrame(this.#resortRaf);
|
|
12442
|
+
this.#resortRaf = requestAnimationFrame(() => {
|
|
12443
|
+
this._sortedRows = this._applySort([...this.rows]);
|
|
12444
|
+
this._rebuildFlat();
|
|
12445
|
+
this.requestUpdate();
|
|
12446
|
+
});
|
|
12447
|
+
}
|
|
12448
|
+
}
|
|
12449
|
+
#appendToFlat(appended, startIdx) {
|
|
12450
|
+
if (this.groupBy) {
|
|
12451
|
+
this._rebuildFlat();
|
|
12452
|
+
return;
|
|
12453
|
+
}
|
|
12454
|
+
for (let j = 0; j < appended.length; j++) {
|
|
12455
|
+
const idx = startIdx + j;
|
|
12456
|
+
const key = this.getRowKey(appended[j], idx);
|
|
12457
|
+
this._flat.push({
|
|
12458
|
+
type: `row`,
|
|
12459
|
+
key,
|
|
12460
|
+
row: appended[j],
|
|
12461
|
+
idx
|
|
12462
|
+
});
|
|
12463
|
+
this._flatKeys.push(key);
|
|
12464
|
+
}
|
|
12465
|
+
}
|
|
11375
12466
|
_replaceSelection(key) {
|
|
11376
12467
|
if (this.selectionMode === `none`) return;
|
|
11377
12468
|
const prev = new Set(this._selectedKeys);
|
|
@@ -11727,6 +12818,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11727
12818
|
return last >= 0 ? this._colOffsets[last] + (this._visibleColumns[last].width ?? 150) : 0;
|
|
11728
12819
|
})()}px"></div>` : A}
|
|
11729
12820
|
</div>
|
|
12821
|
+
${this.columnMenu === `none` ? A : b`
|
|
11730
12822
|
<ixfx-menu-trigger label="⚙" placement="bottom-end" @menu-command=${(e) => {
|
|
11731
12823
|
const cmd = e.detail.command;
|
|
11732
12824
|
const checked = e.detail.checked;
|
|
@@ -11766,7 +12858,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11766
12858
|
</ixfx-menu-radio-group>
|
|
11767
12859
|
` : A}
|
|
11768
12860
|
</ixfx-menu-container>
|
|
11769
|
-
</ixfx-menu-trigger
|
|
12861
|
+
</ixfx-menu-trigger>`}
|
|
11770
12862
|
</div>
|
|
11771
12863
|
<div class="viewport" @scroll=${this._onScroll}>
|
|
11772
12864
|
<div class="spacer" style="height:${this._flat.length * rh}px; width:${this._colWidthSum}px">
|
|
@@ -11783,7 +12875,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11783
12875
|
const selected = this._selectedKeys.has(rowKey);
|
|
11784
12876
|
const tickled = this._tickled.isTickled(rowKey);
|
|
11785
12877
|
return b`
|
|
11786
|
-
<div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``}" style="top:${absIdx * rh}px; height:${rh}px"
|
|
12878
|
+
<div class="row item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${this.#enteringRowKeys.has(rowKey) ? `entering` : ``}" style="top:${absIdx * rh}px; height:${rh}px"
|
|
11787
12879
|
data-key=${rowKey}
|
|
11788
12880
|
@click=${(e) => {
|
|
11789
12881
|
this._onRowClick(rowKey, null, e);
|
|
@@ -11916,6 +13008,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
11916
13008
|
})}
|
|
11917
13009
|
</div>
|
|
11918
13010
|
</div>
|
|
13011
|
+
${this.search.renderField()}
|
|
11919
13012
|
</div>
|
|
11920
13013
|
`;
|
|
11921
13014
|
}
|
|
@@ -11954,7 +13047,18 @@ __decorate([property({
|
|
|
11954
13047
|
attribute: `cross-group-selection`,
|
|
11955
13048
|
reflect: true
|
|
11956
13049
|
})], DataGridElement.prototype, "crossGroupSelection", void 0);
|
|
13050
|
+
__decorate([property({
|
|
13051
|
+
type: String,
|
|
13052
|
+
attribute: `column-menu`
|
|
13053
|
+
})], DataGridElement.prototype, "columnMenu", void 0);
|
|
11957
13054
|
__decorate([property({ attribute: false })], DataGridElement.prototype, "globalFormatter", void 0);
|
|
13055
|
+
__decorate([property({ attribute: false })], DataGridElement.prototype, "filterPredicate", void 0);
|
|
13056
|
+
__decorate([property({ attribute: false })], DataGridElement.prototype, "rowSearchText", void 0);
|
|
13057
|
+
__decorate([property({
|
|
13058
|
+
type: String,
|
|
13059
|
+
attribute: `search-modes`,
|
|
13060
|
+
reflect: true
|
|
13061
|
+
})], DataGridElement.prototype, "searchModes", void 0);
|
|
11958
13062
|
__decorate([state()], DataGridElement.prototype, "_selectedKeys", void 0);
|
|
11959
13063
|
__decorate([state()], DataGridElement.prototype, "_scrollTop", void 0);
|
|
11960
13064
|
__decorate([state()], DataGridElement.prototype, "_scrollLeft", void 0);
|
|
@@ -11970,13 +13074,278 @@ __decorate([query(`.viewport`)], DataGridElement.prototype, "_viewportEl", void
|
|
|
11970
13074
|
__decorate([query(`.header-scroll`)], DataGridElement.prototype, "_headerScrollEl", void 0);
|
|
11971
13075
|
DataGridElement = __decorate([safeCustomElement(`ixfx-data-grid`)], DataGridElement);
|
|
11972
13076
|
//#endregion
|
|
13077
|
+
//#region src/data-table/data-table.ts
|
|
13078
|
+
let DataTableElement = class DataTableElement extends i {
|
|
13079
|
+
constructor(..._args) {
|
|
13080
|
+
super(..._args);
|
|
13081
|
+
this.rows = [];
|
|
13082
|
+
this.tickleDelay = 400;
|
|
13083
|
+
this.layout = `auto`;
|
|
13084
|
+
this.narrowBreakpoint = 420;
|
|
13085
|
+
this._tickled = new TickledController(this, { getItems: () => this.rows.map((_, i) => ({
|
|
13086
|
+
id: i,
|
|
13087
|
+
enabled: true
|
|
13088
|
+
})) });
|
|
13089
|
+
this._stacked = false;
|
|
13090
|
+
this.#lastWidth = 0;
|
|
13091
|
+
}
|
|
13092
|
+
static {
|
|
13093
|
+
this.styles = [
|
|
13094
|
+
tickledStyles,
|
|
13095
|
+
tickledItemStyles,
|
|
13096
|
+
i$3`
|
|
13097
|
+
:host {
|
|
13098
|
+
display: block;
|
|
13099
|
+
container-type: inline-size;
|
|
13100
|
+
}
|
|
13101
|
+
|
|
13102
|
+
.properties {
|
|
13103
|
+
display: grid;
|
|
13104
|
+
grid-template-columns: var(--data-table-name-width, max-content)
|
|
13105
|
+
var(--data-table-value-width, minmax(0, 1fr));
|
|
13106
|
+
/* No column gap: the tickled highlight reads as one solid block.
|
|
13107
|
+
Separation comes from cell padding, which the highlight covers. */
|
|
13108
|
+
gap: 2px 0;
|
|
13109
|
+
}
|
|
13110
|
+
|
|
13111
|
+
/* Widening: 2 then 3 property-pairs per row on roomy containers.
|
|
13112
|
+
Suppressed when stacked, or when locked to 'columns'. */
|
|
13113
|
+
@container (min-width: 650px) {
|
|
13114
|
+
:host(:not([layout='columns'])) .properties:not(.stacked) {
|
|
13115
|
+
grid-template-columns:
|
|
13116
|
+
var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
|
|
13117
|
+
var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr));
|
|
13118
|
+
}
|
|
13119
|
+
}
|
|
13120
|
+
|
|
13121
|
+
@container (min-width: 1000px) {
|
|
13122
|
+
:host(:not([layout='columns'])) .properties:not(.stacked) {
|
|
13123
|
+
grid-template-columns:
|
|
13124
|
+
var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
|
|
13125
|
+
var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr))
|
|
13126
|
+
var(--data-table-name-width, max-content) var(--data-table-value-width, minmax(0, 1fr));
|
|
13127
|
+
}
|
|
13128
|
+
}
|
|
13129
|
+
|
|
13130
|
+
/* Stacked (key above value). Threshold is JS-driven via 'narrowBreakpoint'.
|
|
13131
|
+
minmax(0, 1fr) — not 1fr — so a long value's min-content size can't
|
|
13132
|
+
blow the column out past the container; the value then ellipsises. */
|
|
13133
|
+
.properties.stacked {
|
|
13134
|
+
grid-template-columns: minmax(0, 1fr);
|
|
13135
|
+
}
|
|
13136
|
+
|
|
13137
|
+
/* A pair is its own subgrid so the whole row is one element:
|
|
13138
|
+
the tickled highlight can't split, and key/value share a baseline. */
|
|
13139
|
+
.pair {
|
|
13140
|
+
display: grid;
|
|
13141
|
+
grid-column: span 2;
|
|
13142
|
+
grid-template-columns: subgrid;
|
|
13143
|
+
align-items: var(--data-table-cell-align, baseline);
|
|
13144
|
+
cursor: pointer;
|
|
13145
|
+
}
|
|
13146
|
+
|
|
13147
|
+
.pair[data-full] {
|
|
13148
|
+
grid-column: 1 / -1;
|
|
13149
|
+
}
|
|
13150
|
+
|
|
13151
|
+
.pair[data-full] > .value {
|
|
13152
|
+
grid-column: 2 / -1;
|
|
13153
|
+
}
|
|
13154
|
+
|
|
13155
|
+
.properties.stacked .pair {
|
|
13156
|
+
display: block;
|
|
13157
|
+
grid-column: 1 / -1;
|
|
13158
|
+
/* Let the block shrink below its content so .value-text can clip. */
|
|
13159
|
+
min-width: 0;
|
|
13160
|
+
}
|
|
13161
|
+
|
|
13162
|
+
.properties.stacked .pair:not(:last-child) {
|
|
13163
|
+
margin-bottom: var(--space-s);
|
|
13164
|
+
}
|
|
13165
|
+
|
|
13166
|
+
/* Stacked keys get their own line, so a long label must wrap rather than
|
|
13167
|
+
run off the edge (nowrap is only wanted for the max-content column). */
|
|
13168
|
+
.properties.stacked .key {
|
|
13169
|
+
white-space: normal;
|
|
13170
|
+
overflow-wrap: break-word;
|
|
13171
|
+
}
|
|
13172
|
+
|
|
13173
|
+
.key,
|
|
13174
|
+
.value {
|
|
13175
|
+
min-width: 0;
|
|
13176
|
+
/* Row rhythm comes from this padding (tunable), not line-height. */
|
|
13177
|
+
padding: var(--data-table-row-padding, 0.35em) 0;
|
|
13178
|
+
}
|
|
13179
|
+
|
|
13180
|
+
.key {
|
|
13181
|
+
padding-right: var(--space-m);
|
|
13182
|
+
opacity: 0.6;
|
|
13183
|
+
white-space: nowrap;
|
|
13184
|
+
text-align: var(--data-table-name-align, left);
|
|
13185
|
+
font-size: 10px;
|
|
13186
|
+
text-transform: uppercase;
|
|
13187
|
+
letter-spacing: 0.05em;
|
|
13188
|
+
/* Uppercase keys have no descenders, so both edges can be trimmed to
|
|
13189
|
+
the glyphs for tight baseline / centre alignment. */
|
|
13190
|
+
text-box: trim-both cap alphabetic;
|
|
13191
|
+
}
|
|
13192
|
+
|
|
13193
|
+
.value {
|
|
13194
|
+
position: relative;
|
|
13195
|
+
padding-right: var(--space-l);
|
|
13196
|
+
font: 12px ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
13197
|
+
}
|
|
13198
|
+
|
|
13199
|
+
/* The value text is clipped here — never on .value itself — so a long
|
|
13200
|
+
value stays within its cell even while tickled, and the detail
|
|
13201
|
+
popover (a child of .value) can still escape the cell. */
|
|
13202
|
+
.value-text {
|
|
13203
|
+
display: block;
|
|
13204
|
+
overflow: hidden;
|
|
13205
|
+
text-overflow: ellipsis;
|
|
13206
|
+
white-space: nowrap;
|
|
13207
|
+
/* No text-box trim here: the natural line box gives descenders the room
|
|
13208
|
+
overflow:hidden would otherwise clip. Baseline alignment of key and
|
|
13209
|
+
value comes from align-items on .pair, which needs no trimming. */
|
|
13210
|
+
}
|
|
13211
|
+
|
|
13212
|
+
.pair.tickled {
|
|
13213
|
+
background: var(--item-bg-tickled, color-mix(in oklch, var(--surface-2), var(--accent) 10%));
|
|
13214
|
+
}
|
|
13215
|
+
|
|
13216
|
+
/* Lift the row whose detail is open above the rows it slides over. */
|
|
13217
|
+
.pair.show-detail {
|
|
13218
|
+
position: relative;
|
|
13219
|
+
z-index: 1;
|
|
13220
|
+
}
|
|
13221
|
+
|
|
13222
|
+
.detail {
|
|
13223
|
+
position: absolute;
|
|
13224
|
+
z-index: 2;
|
|
13225
|
+
/* Flush with the bottom of the value (no gap) so the pointer can travel
|
|
13226
|
+
from value to panel without crossing a dead zone that would dismiss it.
|
|
13227
|
+
Left-aligned with the value; only as wide as its content, capped at
|
|
13228
|
+
the table width. */
|
|
13229
|
+
top: 100%;
|
|
13230
|
+
left: 0;
|
|
13231
|
+
right: auto;
|
|
13232
|
+
width: max-content;
|
|
13233
|
+
max-width: 100cqi;
|
|
13234
|
+
padding: var(--space-xs);
|
|
13235
|
+
background: var(--surface-2);
|
|
13236
|
+
visibility: hidden;
|
|
13237
|
+
transform: translateY(-4px);
|
|
13238
|
+
pointer-events: none;
|
|
13239
|
+
transition: transform var(--transition);
|
|
13240
|
+
}
|
|
13241
|
+
|
|
13242
|
+
.pair.show-detail .detail {
|
|
13243
|
+
visibility: visible;
|
|
13244
|
+
transform: translateY(0);
|
|
13245
|
+
pointer-events: auto;
|
|
13246
|
+
}
|
|
13247
|
+
`
|
|
13248
|
+
];
|
|
13249
|
+
}
|
|
13250
|
+
#detailTimer;
|
|
13251
|
+
#resizeObserver;
|
|
13252
|
+
#lastWidth;
|
|
13253
|
+
connectedCallback() {
|
|
13254
|
+
super.connectedCallback();
|
|
13255
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
13256
|
+
const entry = entries[0];
|
|
13257
|
+
if (!entry) return;
|
|
13258
|
+
this.#lastWidth = entry.borderBoxSize?.[0]?.inlineSize ?? entry.contentRect.width;
|
|
13259
|
+
this.#updateStacked();
|
|
13260
|
+
});
|
|
13261
|
+
this.#resizeObserver.observe(this);
|
|
13262
|
+
}
|
|
13263
|
+
disconnectedCallback() {
|
|
13264
|
+
super.disconnectedCallback();
|
|
13265
|
+
clearTimeout(this.#detailTimer);
|
|
13266
|
+
this.#resizeObserver?.disconnect();
|
|
13267
|
+
this.#resizeObserver = void 0;
|
|
13268
|
+
}
|
|
13269
|
+
willUpdate(changed) {
|
|
13270
|
+
if (changed.has(`layout`) || changed.has(`narrowBreakpoint`)) this.#updateStacked();
|
|
13271
|
+
}
|
|
13272
|
+
#updateStacked() {
|
|
13273
|
+
const stacked = this.layout === `rows` ? true : this.layout === `columns` ? false : this.#lastWidth > 0 && this.#lastWidth <= this.narrowBreakpoint;
|
|
13274
|
+
if (stacked !== this._stacked) this._stacked = stacked;
|
|
13275
|
+
}
|
|
13276
|
+
#handlePointerEnter(i) {
|
|
13277
|
+
this._tickled.handlePointerEnter(i);
|
|
13278
|
+
clearTimeout(this.#detailTimer);
|
|
13279
|
+
if (this._detailIndex !== void 0) this._detailIndex = void 0;
|
|
13280
|
+
this.#detailTimer = setTimeout(() => {
|
|
13281
|
+
if (this._tickled.isTickled(i)) this._detailIndex = i;
|
|
13282
|
+
}, this.tickleDelay);
|
|
13283
|
+
}
|
|
13284
|
+
#handlePointerLeave(e) {
|
|
13285
|
+
clearTimeout(this.#detailTimer);
|
|
13286
|
+
this._detailIndex = void 0;
|
|
13287
|
+
this._tickled.handlePointerLeave(e);
|
|
13288
|
+
}
|
|
13289
|
+
#select(i) {
|
|
13290
|
+
clearTimeout(this.#detailTimer);
|
|
13291
|
+
this.selectedIndex = this.selectedIndex === i ? void 0 : i;
|
|
13292
|
+
this._tickled.setTickled(i);
|
|
13293
|
+
this._detailIndex = i;
|
|
13294
|
+
this.dispatchEvent(new CustomEvent(`data-table-row-click`, {
|
|
13295
|
+
bubbles: true,
|
|
13296
|
+
composed: true,
|
|
13297
|
+
detail: {
|
|
13298
|
+
row: this.rows[i],
|
|
13299
|
+
index: i,
|
|
13300
|
+
selected: this.selectedIndex === i
|
|
13301
|
+
}
|
|
13302
|
+
}));
|
|
13303
|
+
}
|
|
13304
|
+
render() {
|
|
13305
|
+
return b`<div
|
|
13306
|
+
class="properties ${this._stacked ? `stacked` : ``}"
|
|
13307
|
+
@pointerleave=${(e) => this.#handlePointerLeave(e)}
|
|
13308
|
+
>
|
|
13309
|
+
${this.rows.map((r, i) => b`<div
|
|
13310
|
+
class="pair ${this._tickled.isTickled(i) ? `tickled` : ``} ${this._detailIndex === i ? `show-detail` : ``}"
|
|
13311
|
+
?data-full=${r.fullWidth}
|
|
13312
|
+
@pointerenter=${() => this.#handlePointerEnter(i)}
|
|
13313
|
+
@click=${() => this.#select(i)}
|
|
13314
|
+
><div class="key">${r.label}</div><div class="value"><span class="value-text">${typeof r.value === `boolean` ? r.value ? `Yes` : `No` : r.value}</span>${r.detail === void 0 ? A : b`<div class="detail" part="detail">${r.detail}</div>`}</div></div>`)}</div>`;
|
|
13315
|
+
}
|
|
13316
|
+
};
|
|
13317
|
+
__decorate([property({
|
|
13318
|
+
type: Array,
|
|
13319
|
+
attribute: false
|
|
13320
|
+
})], DataTableElement.prototype, "rows", void 0);
|
|
13321
|
+
__decorate([property({
|
|
13322
|
+
type: Number,
|
|
13323
|
+
attribute: false
|
|
13324
|
+
})], DataTableElement.prototype, "selectedIndex", void 0);
|
|
13325
|
+
__decorate([property({
|
|
13326
|
+
type: Number,
|
|
13327
|
+
attribute: `tickle-delay`
|
|
13328
|
+
})], DataTableElement.prototype, "tickleDelay", void 0);
|
|
13329
|
+
__decorate([property({
|
|
13330
|
+
type: String,
|
|
13331
|
+
reflect: true
|
|
13332
|
+
})], DataTableElement.prototype, "layout", void 0);
|
|
13333
|
+
__decorate([property({
|
|
13334
|
+
type: Number,
|
|
13335
|
+
attribute: `narrow-breakpoint`
|
|
13336
|
+
})], DataTableElement.prototype, "narrowBreakpoint", void 0);
|
|
13337
|
+
__decorate([state()], DataTableElement.prototype, "_tickled", void 0);
|
|
13338
|
+
__decorate([state()], DataTableElement.prototype, "_detailIndex", void 0);
|
|
13339
|
+
__decorate([state()], DataTableElement.prototype, "_stacked", void 0);
|
|
13340
|
+
DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableElement);
|
|
13341
|
+
//#endregion
|
|
11973
13342
|
//#region src/detail-list/detail-list.ts
|
|
11974
|
-
const ITEM_SELECTOR$
|
|
13343
|
+
const ITEM_SELECTOR$2 = `:scope > li`;
|
|
11975
13344
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
11976
13345
|
const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
|
|
11977
13346
|
let DetailListElement = class DetailListElement extends i {
|
|
11978
|
-
constructor(...
|
|
11979
|
-
super(...
|
|
13347
|
+
constructor(..._args) {
|
|
13348
|
+
super(..._args);
|
|
11980
13349
|
this.selectionMode = `single`;
|
|
11981
13350
|
this.interactionMode = `standard`;
|
|
11982
13351
|
this.sort = `none`;
|
|
@@ -11986,12 +13355,11 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
11986
13355
|
this.iconProperty = ``;
|
|
11987
13356
|
this.keyProperty = ``;
|
|
11988
13357
|
this.itemWrap = `stop`;
|
|
11989
|
-
this._searchVisible = false;
|
|
11990
|
-
this._searchQuery = ``;
|
|
11991
13358
|
this.#itemData = /* @__PURE__ */ new WeakMap();
|
|
11992
13359
|
this.#grid = [];
|
|
11993
13360
|
this.#positions = /* @__PURE__ */ new Map();
|
|
11994
13361
|
this.#geometryScheduled = false;
|
|
13362
|
+
this.#animatedItems = /* @__PURE__ */ new WeakSet();
|
|
11995
13363
|
this.#builtFromProvider = [];
|
|
11996
13364
|
this.#providerBuilt = 0;
|
|
11997
13365
|
this.#data = new DataController(this);
|
|
@@ -12002,11 +13370,13 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12002
13370
|
selectionMode: this.selectionMode,
|
|
12003
13371
|
interactionMode: this.interactionMode
|
|
12004
13372
|
}),
|
|
13373
|
+
getKey: keyForElement,
|
|
12005
13374
|
onChange: (selected, previous) => {
|
|
12006
13375
|
this._syncItemStates();
|
|
12007
13376
|
this._dispatch(`list-select`, {
|
|
12008
13377
|
selected,
|
|
12009
|
-
previous
|
|
13378
|
+
previous,
|
|
13379
|
+
selectedKeys: this.selection.selectedKeys
|
|
12010
13380
|
});
|
|
12011
13381
|
}
|
|
12012
13382
|
});
|
|
@@ -12021,6 +13391,11 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12021
13391
|
onStateChange: () => this._syncItemStates(),
|
|
12022
13392
|
itemWrap: this.itemWrap
|
|
12023
13393
|
});
|
|
13394
|
+
this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
|
|
13395
|
+
itemSelector: ITEM_SELECTOR$2,
|
|
13396
|
+
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
13397
|
+
highlightKey: SEARCH_HIGHLIGHT_KEY
|
|
13398
|
+
}) });
|
|
12024
13399
|
this._onPointerOver = (e) => {
|
|
12025
13400
|
const item = this._findListItem(e.composedPath());
|
|
12026
13401
|
if (item) this.tickled.handlePointerEnter(item);
|
|
@@ -12029,11 +13404,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12029
13404
|
this.tickled.handlePointerLeave(e);
|
|
12030
13405
|
};
|
|
12031
13406
|
this._onKeyDown = (e) => {
|
|
12032
|
-
if (
|
|
12033
|
-
e.preventDefault();
|
|
12034
|
-
this._openSearch();
|
|
12035
|
-
return;
|
|
12036
|
-
}
|
|
13407
|
+
if (this.search.isOpen) return;
|
|
12037
13408
|
if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
|
|
12038
13409
|
if (this.selection.canSelectAll()) {
|
|
12039
13410
|
e.preventDefault();
|
|
@@ -12041,11 +13412,6 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12041
13412
|
}
|
|
12042
13413
|
return;
|
|
12043
13414
|
}
|
|
12044
|
-
if (this._searchVisible && e.key === `Escape`) {
|
|
12045
|
-
e.preventDefault();
|
|
12046
|
-
this._closeSearch();
|
|
12047
|
-
return;
|
|
12048
|
-
}
|
|
12049
13415
|
switch (e.key) {
|
|
12050
13416
|
case `ArrowDown`:
|
|
12051
13417
|
e.preventDefault();
|
|
@@ -12114,53 +13480,36 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12114
13480
|
e.preventDefault();
|
|
12115
13481
|
this._listEl.scrollLeft += e.deltaY;
|
|
12116
13482
|
};
|
|
12117
|
-
this._onSearchInput = (e) => {
|
|
12118
|
-
const input = e.target;
|
|
12119
|
-
this._searchQuery = input.value;
|
|
12120
|
-
this.#search?.setQuery(this._searchQuery).then(() => this.#recomputeGeometry());
|
|
12121
|
-
};
|
|
12122
|
-
this._onSearchKeydown = (e) => {
|
|
12123
|
-
if (e.key === `Escape`) {
|
|
12124
|
-
e.stopPropagation();
|
|
12125
|
-
this._closeSearch();
|
|
12126
|
-
} else if (e.key === `ArrowDown`) {
|
|
12127
|
-
e.preventDefault();
|
|
12128
|
-
this.focus();
|
|
12129
|
-
this.#navigateDown();
|
|
12130
|
-
} else if (e.key === `ArrowUp`) {
|
|
12131
|
-
e.preventDefault();
|
|
12132
|
-
this.focus();
|
|
12133
|
-
this.#navigateUp();
|
|
12134
|
-
} else if (e.key === `ArrowRight`) e.preventDefault();
|
|
12135
|
-
else if (e.key === `ArrowLeft`) e.preventDefault();
|
|
12136
|
-
else if (e.key === `Enter`) {
|
|
12137
|
-
e.preventDefault();
|
|
12138
|
-
e.stopPropagation();
|
|
12139
|
-
const id = this.tickled.tickledId ?? this.#findFirstCheckable();
|
|
12140
|
-
if (id) {
|
|
12141
|
-
this._dispatch(`list-item-click`, { item: id });
|
|
12142
|
-
this._activateItem(id);
|
|
12143
|
-
this._closeSearch();
|
|
12144
|
-
}
|
|
12145
|
-
} else if (e.key === ` `) {
|
|
12146
|
-
const id = this.tickled.tickledId;
|
|
12147
|
-
if (id) {
|
|
12148
|
-
e.preventDefault();
|
|
12149
|
-
this.selection.handleSpace(id);
|
|
12150
|
-
}
|
|
12151
|
-
}
|
|
12152
|
-
};
|
|
12153
13483
|
}
|
|
12154
|
-
#search;
|
|
12155
13484
|
#itemData;
|
|
12156
13485
|
#grid;
|
|
12157
13486
|
#positions;
|
|
12158
13487
|
#geometryScheduled;
|
|
12159
13488
|
#resizeObserver;
|
|
12160
13489
|
#mutationObserver;
|
|
13490
|
+
/** Items that have already played (or been skipped for) their enter animation. */
|
|
13491
|
+
#animatedItems;
|
|
12161
13492
|
#builtFromProvider;
|
|
12162
13493
|
#providerBuilt;
|
|
12163
13494
|
#data;
|
|
13495
|
+
#searchRegistry;
|
|
13496
|
+
#unregisterSearchCommands;
|
|
13497
|
+
/** Command registry carrying this list's `list.search.*` commands. */
|
|
13498
|
+
get registry() {
|
|
13499
|
+
return this.#searchRegistry;
|
|
13500
|
+
}
|
|
13501
|
+
#effectiveSearchModes() {
|
|
13502
|
+
const raw = this.searchModes?.trim();
|
|
13503
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
13504
|
+
const requested = new Set(raw.split(/\s+/));
|
|
13505
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
13506
|
+
}
|
|
13507
|
+
#syncSearchCommands() {
|
|
13508
|
+
if (!this.#searchRegistry) return;
|
|
13509
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
13510
|
+
this.#unregisterSearchCommands?.();
|
|
13511
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
13512
|
+
}
|
|
12164
13513
|
connectedCallback() {
|
|
12165
13514
|
super.connectedCallback();
|
|
12166
13515
|
if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
|
|
@@ -12172,8 +13521,9 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12172
13521
|
this.addEventListener(`wheel`, this._onWheel);
|
|
12173
13522
|
this.#resizeObserver = new ResizeObserver(() => this.#scheduleGeometry());
|
|
12174
13523
|
this.#resizeObserver.observe(this);
|
|
12175
|
-
this.#mutationObserver = new MutationObserver(() => {
|
|
13524
|
+
this.#mutationObserver = new MutationObserver((records) => {
|
|
12176
13525
|
this.#processChildren();
|
|
13526
|
+
this.#animateChildMutations(records);
|
|
12177
13527
|
this.#scheduleGeometry();
|
|
12178
13528
|
});
|
|
12179
13529
|
this.#mutationObserver.observe(this, {
|
|
@@ -12181,6 +13531,29 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12181
13531
|
subtree: false,
|
|
12182
13532
|
attributeFilter: [`hidden`]
|
|
12183
13533
|
});
|
|
13534
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#animatedItems.add(el);
|
|
13535
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
13536
|
+
this.#syncSearchCommands();
|
|
13537
|
+
}
|
|
13538
|
+
/**
|
|
13539
|
+
* Collapse-in items appended to the light DOM (however they got there). A bulk
|
|
13540
|
+
* populate is left alone. Geometry is re-read once the animations settle so
|
|
13541
|
+
* keyboard navigation lines back up with the final layout.
|
|
13542
|
+
*/
|
|
13543
|
+
#animateChildMutations(records) {
|
|
13544
|
+
if (!this.isConnected) return;
|
|
13545
|
+
const fresh = [];
|
|
13546
|
+
for (const record of records) for (const node of record.addedNodes) {
|
|
13547
|
+
if (!(node instanceof HTMLElement) || node.parentElement !== this || node.localName !== `li`) continue;
|
|
13548
|
+
if (this.#animatedItems.has(node)) continue;
|
|
13549
|
+
this.#animatedItems.add(node);
|
|
13550
|
+
fresh.push(node);
|
|
13551
|
+
}
|
|
13552
|
+
if (fresh.length === 0) return;
|
|
13553
|
+
const total = this.querySelectorAll(ITEM_SELECTOR$2).length;
|
|
13554
|
+
if (fresh.length >= total) return;
|
|
13555
|
+
for (const el of fresh) animateListItemIn(el);
|
|
13556
|
+
window.setTimeout(() => this.#scheduleGeometry(), 230);
|
|
12184
13557
|
}
|
|
12185
13558
|
disconnectedCallback() {
|
|
12186
13559
|
super.disconnectedCallback();
|
|
@@ -12194,23 +13567,18 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12194
13567
|
this.#resizeObserver = void 0;
|
|
12195
13568
|
this.#mutationObserver?.disconnect();
|
|
12196
13569
|
this.#mutationObserver = void 0;
|
|
12197
|
-
this.#
|
|
12198
|
-
this.#
|
|
13570
|
+
this.#unregisterSearchCommands?.();
|
|
13571
|
+
this.#unregisterSearchCommands = void 0;
|
|
13572
|
+
this.#searchRegistry = void 0;
|
|
12199
13573
|
}
|
|
12200
13574
|
firstUpdated() {
|
|
12201
13575
|
this.#processChildren();
|
|
12202
13576
|
if (this.filterPredicate) this.#applyFilterPredicate();
|
|
12203
|
-
this.#search = createElementIncrSearch({
|
|
12204
|
-
container: this,
|
|
12205
|
-
itemSelector: ITEM_SELECTOR$1,
|
|
12206
|
-
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
12207
|
-
getKey: (el) => el.getAttribute(`data-key`) ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
12208
|
-
highlightKey: SEARCH_HIGHLIGHT_KEY
|
|
12209
|
-
});
|
|
12210
13577
|
this.#recomputeGeometry();
|
|
12211
13578
|
}
|
|
12212
13579
|
updated(changed) {
|
|
12213
13580
|
super.updated(changed);
|
|
13581
|
+
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
12214
13582
|
if (changed.has(`provider`)) {
|
|
12215
13583
|
this.#builtFromProvider = [];
|
|
12216
13584
|
this.#providerBuilt = 0;
|
|
@@ -12261,8 +13629,21 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12261
13629
|
if (this.sort !== `none`) this.#applySort();
|
|
12262
13630
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
12263
13631
|
}
|
|
13632
|
+
/**
|
|
13633
|
+
* Collapse + fade `item` out, then detach it; the rows after it slide up to
|
|
13634
|
+
* close the gap. Resolves once removed. Immediate under reduced motion.
|
|
13635
|
+
*/
|
|
12264
13636
|
removeItem(item) {
|
|
12265
|
-
|
|
13637
|
+
this.selection.deselect(item);
|
|
13638
|
+
return animateListItemOut(item).then(() => {
|
|
13639
|
+
item.remove();
|
|
13640
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
13641
|
+
this.#recomputeGeometry();
|
|
13642
|
+
});
|
|
13643
|
+
}
|
|
13644
|
+
/** {@link removeItem} for several items at once. */
|
|
13645
|
+
removeItems(items) {
|
|
13646
|
+
return Promise.all([...items].map((item) => this.removeItem(item))).then(() => void 0);
|
|
12266
13647
|
}
|
|
12267
13648
|
clearItems() {
|
|
12268
13649
|
this.replaceChildren();
|
|
@@ -12291,8 +13672,14 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12291
13672
|
getItemData(item) {
|
|
12292
13673
|
return this.#itemData.get(item);
|
|
12293
13674
|
}
|
|
12294
|
-
/**
|
|
12295
|
-
|
|
13675
|
+
/**
|
|
13676
|
+
* Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
|
|
13677
|
+
* forward commands. Routes through the command registry (so `list.search.*`
|
|
13678
|
+
* and any other registered commands work).
|
|
13679
|
+
*/
|
|
13680
|
+
dispatch(commandId, args) {
|
|
13681
|
+
this.#searchRegistry?.invoke(commandId, args);
|
|
13682
|
+
}
|
|
12296
13683
|
select(item) {
|
|
12297
13684
|
this.selection.replace(item);
|
|
12298
13685
|
}
|
|
@@ -12308,6 +13695,40 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12308
13695
|
selectAll() {
|
|
12309
13696
|
if (this.selection.canSelectAll()) this.selection.selectAll();
|
|
12310
13697
|
}
|
|
13698
|
+
/** The selection as stable `data-key` / `data-value` strings. */
|
|
13699
|
+
get selectedKeys() {
|
|
13700
|
+
return this.selection.selectedKeys;
|
|
13701
|
+
}
|
|
13702
|
+
/** The key of the item under the cursor, or `undefined`. */
|
|
13703
|
+
get tickledKey() {
|
|
13704
|
+
const id = this.tickled.tickledId;
|
|
13705
|
+
return id ? keyForElement(id) : void 0;
|
|
13706
|
+
}
|
|
13707
|
+
/** Replace the selection with the items carrying these keys (unknown keys ignored). */
|
|
13708
|
+
selectKeys(keys) {
|
|
13709
|
+
this.selection.selectKeys(keys, (key) => this.#itemForKey(key));
|
|
13710
|
+
}
|
|
13711
|
+
/** Add items by key to the selection (multi-select modes only). */
|
|
13712
|
+
addSelectedKeys(keys) {
|
|
13713
|
+
this.selection.addKeys(keys, (key) => this.#itemForKey(key));
|
|
13714
|
+
}
|
|
13715
|
+
/** Remove items by key from the selection. */
|
|
13716
|
+
removeSelectedKeys(keys) {
|
|
13717
|
+
this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
|
|
13718
|
+
}
|
|
13719
|
+
/** Move the cursor to the item with this key and scroll it into view. */
|
|
13720
|
+
focusKey(key) {
|
|
13721
|
+
const el = this.#itemForKey(key);
|
|
13722
|
+
if (!el) return;
|
|
13723
|
+
this.tickled.focus(el);
|
|
13724
|
+
el.scrollIntoView({
|
|
13725
|
+
block: `nearest`,
|
|
13726
|
+
inline: `nearest`
|
|
13727
|
+
});
|
|
13728
|
+
}
|
|
13729
|
+
#itemForKey(key) {
|
|
13730
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (keyForElement(el) === key) return el;
|
|
13731
|
+
}
|
|
12311
13732
|
*selectedFilter(kind) {
|
|
12312
13733
|
const wanted = (el) => {
|
|
12313
13734
|
switch (kind) {
|
|
@@ -12316,7 +13737,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12316
13737
|
case `either`: return el.hasAttribute(`data-selected`) || el.hasAttribute(`data-checked`);
|
|
12317
13738
|
}
|
|
12318
13739
|
};
|
|
12319
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
13740
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
|
|
12320
13741
|
if (el.hidden) continue;
|
|
12321
13742
|
if (wanted(el)) yield el;
|
|
12322
13743
|
}
|
|
@@ -12329,7 +13750,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12329
13750
|
if (previousKeys.size > 0 || anchorKey !== null) {
|
|
12330
13751
|
const next = [];
|
|
12331
13752
|
let anchor;
|
|
12332
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
13753
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
|
|
12333
13754
|
const key = keyOf(el);
|
|
12334
13755
|
if (key !== null && previousKeys.has(key)) next.push(el);
|
|
12335
13756
|
if (key !== null && key === anchorKey) anchor = el;
|
|
@@ -12384,15 +13805,15 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12384
13805
|
}
|
|
12385
13806
|
#applyFilterPredicate() {
|
|
12386
13807
|
if (!this.filterPredicate) {
|
|
12387
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
13808
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) el.removeAttribute(`hidden`);
|
|
12388
13809
|
return;
|
|
12389
13810
|
}
|
|
12390
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
13811
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) if (this.filterPredicate(el)) el.removeAttribute(`hidden`);
|
|
12391
13812
|
else el.setAttribute(`hidden`, ``);
|
|
12392
13813
|
}
|
|
12393
13814
|
#applySort() {
|
|
12394
13815
|
if (this.sort === `none`) return;
|
|
12395
|
-
const rows = [...this.querySelectorAll(ITEM_SELECTOR$
|
|
13816
|
+
const rows = [...this.querySelectorAll(ITEM_SELECTOR$2)];
|
|
12396
13817
|
if (rows.length < 2) return;
|
|
12397
13818
|
const dir = this.sort === `descending` ? -1 : 1;
|
|
12398
13819
|
const meta = (el) => ({
|
|
@@ -12584,7 +14005,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12584
14005
|
else if (this.itemWrap === `wrap`) this.#navigateTo(this.#findLastCheckable());
|
|
12585
14006
|
}
|
|
12586
14007
|
_getVisibleItems() {
|
|
12587
|
-
return Array.from(this.querySelectorAll(ITEM_SELECTOR$
|
|
14008
|
+
return Array.from(this.querySelectorAll(ITEM_SELECTOR$2)).filter((el) => {
|
|
12588
14009
|
if (el.hidden) return false;
|
|
12589
14010
|
if (this.filterPredicate && !this.filterPredicate(el)) return false;
|
|
12590
14011
|
return true;
|
|
@@ -12639,10 +14060,10 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12639
14060
|
}
|
|
12640
14061
|
/** Scan light-DOM children for unprocessed `data-icon` items. */
|
|
12641
14062
|
#processChildren() {
|
|
12642
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
14063
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) this.#ensureIcon(el);
|
|
12643
14064
|
}
|
|
12644
14065
|
_syncItemStates() {
|
|
12645
|
-
for (const el of this.querySelectorAll(ITEM_SELECTOR$
|
|
14066
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR$2)) {
|
|
12646
14067
|
el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
|
|
12647
14068
|
el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
|
|
12648
14069
|
if (this.interactionMode === `checked`) el.toggleAttribute(`data-checked`, !el.hasAttribute(`not-checked`) && this.selection.selected.has(el));
|
|
@@ -12742,16 +14163,6 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12742
14163
|
composed: true
|
|
12743
14164
|
}));
|
|
12744
14165
|
}
|
|
12745
|
-
_openSearch() {
|
|
12746
|
-
this._searchVisible = true;
|
|
12747
|
-
this.updateComplete.then(() => this._searchInputEl?.focus());
|
|
12748
|
-
}
|
|
12749
|
-
_closeSearch() {
|
|
12750
|
-
this._searchVisible = false;
|
|
12751
|
-
this._searchQuery = ``;
|
|
12752
|
-
this.#search?.clearQuery().then(() => this.#recomputeGeometry());
|
|
12753
|
-
this.focus();
|
|
12754
|
-
}
|
|
12755
14166
|
_activateItem(item) {
|
|
12756
14167
|
if (this.interactionMode !== `manual`) this.selection.replace(item);
|
|
12757
14168
|
this._dispatch(`list-activate`, { item });
|
|
@@ -12760,6 +14171,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12760
14171
|
this.styles = [
|
|
12761
14172
|
themeFallbacks,
|
|
12762
14173
|
scrollFadeStyles(`x`),
|
|
14174
|
+
listSearchStyles,
|
|
12763
14175
|
i$3`
|
|
12764
14176
|
:host {
|
|
12765
14177
|
display: block;
|
|
@@ -12881,48 +14293,6 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12881
14293
|
.loading[hidden] {
|
|
12882
14294
|
display: none;
|
|
12883
14295
|
}
|
|
12884
|
-
|
|
12885
|
-
.search-overlay {
|
|
12886
|
-
position: absolute;
|
|
12887
|
-
top: 8px;
|
|
12888
|
-
right: 8px;
|
|
12889
|
-
z-index: 10;
|
|
12890
|
-
display: flex;
|
|
12891
|
-
align-items: center;
|
|
12892
|
-
gap: 4px;
|
|
12893
|
-
background: var(--surface-4, #2a2a2a);
|
|
12894
|
-
border: 1px solid var(--border, #444);
|
|
12895
|
-
border-radius: var(--radius-m, 4px);
|
|
12896
|
-
padding: 4px 6px;
|
|
12897
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
12898
|
-
}
|
|
12899
|
-
|
|
12900
|
-
.search-overlay[hidden] {
|
|
12901
|
-
display: none;
|
|
12902
|
-
}
|
|
12903
|
-
|
|
12904
|
-
.search-input {
|
|
12905
|
-
background: transparent;
|
|
12906
|
-
border: none;
|
|
12907
|
-
outline: none;
|
|
12908
|
-
color: inherit;
|
|
12909
|
-
font-size: var(--text-m, 0.875em);
|
|
12910
|
-
min-width: 140px;
|
|
12911
|
-
}
|
|
12912
|
-
|
|
12913
|
-
.search-close {
|
|
12914
|
-
background: transparent;
|
|
12915
|
-
border: none;
|
|
12916
|
-
color: var(--surface-muted-text, #888);
|
|
12917
|
-
cursor: pointer;
|
|
12918
|
-
font-size: 1.1em;
|
|
12919
|
-
line-height: 1;
|
|
12920
|
-
padding: 0 2px;
|
|
12921
|
-
}
|
|
12922
|
-
|
|
12923
|
-
.search-close:hover {
|
|
12924
|
-
color: inherit;
|
|
12925
|
-
}
|
|
12926
14296
|
`
|
|
12927
14297
|
];
|
|
12928
14298
|
}
|
|
@@ -12937,21 +14307,7 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
12937
14307
|
<div class="loading" part="loading" ?hidden=${!loading}>
|
|
12938
14308
|
${loading ? progress !== void 0 ? `${Math.round(progress * 100)}%` : `loading…` : A}
|
|
12939
14309
|
</div>
|
|
12940
|
-
|
|
12941
|
-
class="search-overlay"
|
|
12942
|
-
part="search-overlay"
|
|
12943
|
-
?hidden=${!this._searchVisible}
|
|
12944
|
-
>
|
|
12945
|
-
<input
|
|
12946
|
-
type="text"
|
|
12947
|
-
class="search-input"
|
|
12948
|
-
placeholder="Search…"
|
|
12949
|
-
.value=${this._searchQuery}
|
|
12950
|
-
@input=${this._onSearchInput}
|
|
12951
|
-
@keydown=${this._onSearchKeydown}
|
|
12952
|
-
/>
|
|
12953
|
-
<button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
|
|
12954
|
-
</div>
|
|
14310
|
+
${this.search.renderField()}
|
|
12955
14311
|
</div>
|
|
12956
14312
|
`;
|
|
12957
14313
|
}
|
|
@@ -12991,9 +14347,11 @@ __decorate([property({
|
|
|
12991
14347
|
attribute: `item-wrap`
|
|
12992
14348
|
})], DetailListElement.prototype, "itemWrap", void 0);
|
|
12993
14349
|
__decorate([property({ attribute: false })], DetailListElement.prototype, "checkboxRenderer", void 0);
|
|
12994
|
-
__decorate([
|
|
12995
|
-
|
|
12996
|
-
|
|
14350
|
+
__decorate([property({
|
|
14351
|
+
type: String,
|
|
14352
|
+
attribute: `search-modes`,
|
|
14353
|
+
reflect: true
|
|
14354
|
+
})], DetailListElement.prototype, "searchModes", void 0);
|
|
12997
14355
|
__decorate([query(`ul`)], DetailListElement.prototype, "_listEl", void 0);
|
|
12998
14356
|
DetailListElement = __decorate([safeCustomElement(`ixfx-detail-list`)], DetailListElement);
|
|
12999
14357
|
//#endregion
|
|
@@ -18701,6 +20059,983 @@ let FormElement = class FormElement extends i {
|
|
|
18701
20059
|
};
|
|
18702
20060
|
FormElement = __decorate([safeCustomElement(`ixfx-form`)], FormElement);
|
|
18703
20061
|
//#endregion
|
|
20062
|
+
//#region src/grid-list/grid-list.ts
|
|
20063
|
+
const ITEM_SELECTOR$1 = `:scope > *`;
|
|
20064
|
+
const MIN_ITEM_SIZE = 16;
|
|
20065
|
+
/**
|
|
20066
|
+
* How long the host keeps the `[reflowing]` attribute after items are inserted
|
|
20067
|
+
* mid-list — long enough for the cell move-transition and the enter animation
|
|
20068
|
+
* to finish. Kept a touch above the default `--grid-list-item-move-duration`.
|
|
20069
|
+
*/
|
|
20070
|
+
const REFLOW_WINDOW_MS = 320;
|
|
20071
|
+
function computeGridWindow(count, itemWidth, rowHeight, gap, viewportWidth, viewportHeight, scrollOffset, orientation, overscan = 2) {
|
|
20072
|
+
const w = Math.max(1, itemWidth);
|
|
20073
|
+
const h = Math.max(1, rowHeight);
|
|
20074
|
+
const g = Math.max(0, gap);
|
|
20075
|
+
const colStride = w + g;
|
|
20076
|
+
const rowStride = h + g;
|
|
20077
|
+
const offset = Math.max(0, scrollOffset);
|
|
20078
|
+
if (orientation === `row`) {
|
|
20079
|
+
const rows = count === 0 ? 0 : 1;
|
|
20080
|
+
const totalWidth = count === 0 ? 0 : (count - 1) * colStride + w;
|
|
20081
|
+
if (count === 0 || viewportWidth <= 0) return {
|
|
20082
|
+
cols: count,
|
|
20083
|
+
rows,
|
|
20084
|
+
totalHeight: 0,
|
|
20085
|
+
totalWidth,
|
|
20086
|
+
startIndex: 0,
|
|
20087
|
+
endIndex: 0,
|
|
20088
|
+
colStride,
|
|
20089
|
+
rowStride,
|
|
20090
|
+
horizontal: true
|
|
20091
|
+
};
|
|
20092
|
+
const firstVisible = Math.max(0, Math.floor(offset / colStride));
|
|
20093
|
+
const lastVisible = Math.ceil((offset + viewportWidth) / colStride);
|
|
20094
|
+
return {
|
|
20095
|
+
cols: count,
|
|
20096
|
+
rows,
|
|
20097
|
+
totalHeight: 0,
|
|
20098
|
+
totalWidth,
|
|
20099
|
+
startIndex: Math.max(0, firstVisible - overscan),
|
|
20100
|
+
endIndex: Math.min(count, lastVisible + overscan),
|
|
20101
|
+
colStride,
|
|
20102
|
+
rowStride,
|
|
20103
|
+
horizontal: true
|
|
20104
|
+
};
|
|
20105
|
+
}
|
|
20106
|
+
const cols = orientation === `column` ? 1 : Math.max(1, Math.floor((Math.max(0, viewportWidth) + g) / colStride));
|
|
20107
|
+
const rows = count === 0 ? 0 : Math.ceil(count / cols);
|
|
20108
|
+
const totalHeight = rows === 0 ? 0 : (rows - 1) * rowStride + h;
|
|
20109
|
+
if (count === 0 || viewportHeight <= 0) return {
|
|
20110
|
+
cols,
|
|
20111
|
+
rows,
|
|
20112
|
+
totalHeight,
|
|
20113
|
+
totalWidth: 0,
|
|
20114
|
+
startIndex: 0,
|
|
20115
|
+
endIndex: 0,
|
|
20116
|
+
colStride,
|
|
20117
|
+
rowStride,
|
|
20118
|
+
horizontal: false
|
|
20119
|
+
};
|
|
20120
|
+
const firstVisibleRow = Math.max(0, Math.floor(offset / rowStride));
|
|
20121
|
+
const lastVisibleRow = Math.ceil((offset + viewportHeight) / rowStride);
|
|
20122
|
+
const rowStart = Math.max(0, firstVisibleRow - overscan);
|
|
20123
|
+
const rowEnd = Math.min(rows, lastVisibleRow + overscan);
|
|
20124
|
+
return {
|
|
20125
|
+
cols,
|
|
20126
|
+
rows,
|
|
20127
|
+
totalHeight,
|
|
20128
|
+
totalWidth: 0,
|
|
20129
|
+
startIndex: rowStart * cols,
|
|
20130
|
+
endIndex: Math.min(count, rowEnd * cols),
|
|
20131
|
+
colStride,
|
|
20132
|
+
rowStride,
|
|
20133
|
+
horizontal: false
|
|
20134
|
+
};
|
|
20135
|
+
}
|
|
20136
|
+
function columnOf(index, cols) {
|
|
20137
|
+
return cols <= 1 ? 0 : index % cols;
|
|
20138
|
+
}
|
|
20139
|
+
function rowOf(index, cols) {
|
|
20140
|
+
return cols <= 1 ? index : Math.floor(index / cols);
|
|
20141
|
+
}
|
|
20142
|
+
let GridListElement = class GridListElement extends i {
|
|
20143
|
+
constructor(..._args) {
|
|
20144
|
+
super(..._args);
|
|
20145
|
+
this.selectionMode = `single`;
|
|
20146
|
+
this.interactionMode = `standard`;
|
|
20147
|
+
this.itemSize = 160;
|
|
20148
|
+
this.gap = 8;
|
|
20149
|
+
this.orientation = `grid`;
|
|
20150
|
+
this.captionStyle = `under`;
|
|
20151
|
+
this.captionAlign = `middle`;
|
|
20152
|
+
this.captionPosition = `bottom`;
|
|
20153
|
+
this.captionBackdrop = `scrim`;
|
|
20154
|
+
this.captionHeight = 24;
|
|
20155
|
+
this.itemWrap = `stop`;
|
|
20156
|
+
this._scrollTop = 0;
|
|
20157
|
+
this._scrollLeft = 0;
|
|
20158
|
+
this._viewportHeight = 0;
|
|
20159
|
+
this._viewportWidth = 0;
|
|
20160
|
+
this.#viewportRef = createRef();
|
|
20161
|
+
this.#scrollRaf = 0;
|
|
20162
|
+
this.#items = [];
|
|
20163
|
+
this.#itemWrap = `stop`;
|
|
20164
|
+
this.#entering = /* @__PURE__ */ new Set();
|
|
20165
|
+
this.#reflowTimer = 0;
|
|
20166
|
+
this.#slotNames = /* @__PURE__ */ new WeakMap();
|
|
20167
|
+
this.#slotSeq = 0;
|
|
20168
|
+
this.selection = new ListSelectionController({
|
|
20169
|
+
getOrderedItems: () => this._getVisibleItems(),
|
|
20170
|
+
isSelectable: () => true,
|
|
20171
|
+
getModes: () => ({
|
|
20172
|
+
selectionMode: this.selectionMode,
|
|
20173
|
+
interactionMode: this.interactionMode
|
|
20174
|
+
}),
|
|
20175
|
+
getKey: keyForElement,
|
|
20176
|
+
onChange: (selected, previous) => {
|
|
20177
|
+
this._syncItemStates();
|
|
20178
|
+
this.requestUpdate();
|
|
20179
|
+
this._dispatch(`list-select`, {
|
|
20180
|
+
selected,
|
|
20181
|
+
previous,
|
|
20182
|
+
selectedKeys: this.selection.selectedKeys
|
|
20183
|
+
});
|
|
20184
|
+
}
|
|
20185
|
+
});
|
|
20186
|
+
this.tickled = new TickledController(this, {
|
|
20187
|
+
getItems: () => this.#items.map((el) => ({
|
|
20188
|
+
id: el,
|
|
20189
|
+
enabled: !el.hasAttribute(`not-checked`)
|
|
20190
|
+
})),
|
|
20191
|
+
onTickle: (id) => {
|
|
20192
|
+
if (id !== void 0) this._dispatch(`list-tickle`, { item: id });
|
|
20193
|
+
},
|
|
20194
|
+
onStateChange: () => this._syncItemStates(),
|
|
20195
|
+
itemWrap: this.#itemWrap
|
|
20196
|
+
});
|
|
20197
|
+
this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
|
|
20198
|
+
itemSelector: ITEM_SELECTOR$1,
|
|
20199
|
+
getLabel: (el) => el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
20200
|
+
highlightKey: `grid-list-search`
|
|
20201
|
+
}) });
|
|
20202
|
+
this.#onScroll = (e) => {
|
|
20203
|
+
const el = e.target;
|
|
20204
|
+
this.#cancelScrollRaf();
|
|
20205
|
+
this.#scrollRaf = requestAnimationFrame(() => {
|
|
20206
|
+
this.#scrollRaf = 0;
|
|
20207
|
+
this._scrollTop = el.scrollTop;
|
|
20208
|
+
this._scrollLeft = el.scrollLeft;
|
|
20209
|
+
this.#emitViewportChange();
|
|
20210
|
+
});
|
|
20211
|
+
};
|
|
20212
|
+
this._onKeyDown = (e) => {
|
|
20213
|
+
if ((e.ctrlKey || e.metaKey) && e.key === `f`) return;
|
|
20214
|
+
if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
|
|
20215
|
+
if (this.selection.canSelectAll()) {
|
|
20216
|
+
e.preventDefault();
|
|
20217
|
+
this.selection.selectAll();
|
|
20218
|
+
}
|
|
20219
|
+
return;
|
|
20220
|
+
}
|
|
20221
|
+
const count = this.#items.length;
|
|
20222
|
+
if (count === 0) return;
|
|
20223
|
+
const win = this.#currentWindow();
|
|
20224
|
+
const move = (delta) => {
|
|
20225
|
+
const from = this.tickled.tickledId ? this.#items.indexOf(this.tickled.tickledId) : -1;
|
|
20226
|
+
const nextIndex = Math.max(0, Math.min(count - 1, from + delta));
|
|
20227
|
+
const next = this.#items[nextIndex];
|
|
20228
|
+
if (next) {
|
|
20229
|
+
this.tickled.handleKeyboardNavigation(next);
|
|
20230
|
+
this.#scrollItemIntoView(next);
|
|
20231
|
+
}
|
|
20232
|
+
};
|
|
20233
|
+
const perLine = this.orientation === `grid` ? win.cols : 1;
|
|
20234
|
+
switch (e.key) {
|
|
20235
|
+
case `ArrowLeft`:
|
|
20236
|
+
move(-1);
|
|
20237
|
+
break;
|
|
20238
|
+
case `ArrowRight`:
|
|
20239
|
+
move(1);
|
|
20240
|
+
break;
|
|
20241
|
+
case `ArrowUp`:
|
|
20242
|
+
move(-perLine);
|
|
20243
|
+
break;
|
|
20244
|
+
case `ArrowDown`:
|
|
20245
|
+
move(perLine);
|
|
20246
|
+
break;
|
|
20247
|
+
case `Home`:
|
|
20248
|
+
this.tickled.handleKeyboardNavigation(`first`);
|
|
20249
|
+
if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
|
|
20250
|
+
break;
|
|
20251
|
+
case `End`:
|
|
20252
|
+
this.tickled.handleKeyboardNavigation(`last`);
|
|
20253
|
+
if (this.tickled.tickledId) this.#scrollItemIntoView(this.tickled.tickledId);
|
|
20254
|
+
break;
|
|
20255
|
+
case `PageUp`:
|
|
20256
|
+
case `PageDown`: {
|
|
20257
|
+
const mainExtent = win.horizontal ? this._viewportWidth : this._viewportHeight;
|
|
20258
|
+
const mainStride = win.horizontal ? win.colStride : win.rowStride;
|
|
20259
|
+
const visibleLines = Math.max(1, Math.floor(mainExtent / mainStride));
|
|
20260
|
+
move((e.key === `PageDown` ? 1 : -1) * visibleLines * perLine);
|
|
20261
|
+
break;
|
|
20262
|
+
}
|
|
20263
|
+
case `Enter`: {
|
|
20264
|
+
const id = this.tickled.tickledId;
|
|
20265
|
+
if (id) {
|
|
20266
|
+
e.preventDefault();
|
|
20267
|
+
this._dispatch(`list-item-click`, { item: id });
|
|
20268
|
+
this.#onCellActivate(id);
|
|
20269
|
+
}
|
|
20270
|
+
break;
|
|
20271
|
+
}
|
|
20272
|
+
case ` `: {
|
|
20273
|
+
const id = this.tickled.tickledId;
|
|
20274
|
+
if (id) {
|
|
20275
|
+
e.preventDefault();
|
|
20276
|
+
this.selection.handleSpace(id);
|
|
20277
|
+
}
|
|
20278
|
+
break;
|
|
20279
|
+
}
|
|
20280
|
+
case `Escape`:
|
|
20281
|
+
this.selection.clear();
|
|
20282
|
+
break;
|
|
20283
|
+
default: return;
|
|
20284
|
+
}
|
|
20285
|
+
e.preventDefault();
|
|
20286
|
+
};
|
|
20287
|
+
}
|
|
20288
|
+
static {
|
|
20289
|
+
this.styles = [
|
|
20290
|
+
themeFallbacks,
|
|
20291
|
+
tickledStyles,
|
|
20292
|
+
tickledItemStyles,
|
|
20293
|
+
scrollFadeStyles(`x`),
|
|
20294
|
+
scrollFadeStyles(`y`),
|
|
20295
|
+
listItemFadeKeyframes,
|
|
20296
|
+
listSearchStyles,
|
|
20297
|
+
i$3`
|
|
20298
|
+
:host {
|
|
20299
|
+
--grid-list-item-bg: var(--item-bg);
|
|
20300
|
+
--grid-list-radius: var(--radius-m);
|
|
20301
|
+
--grid-list-gap: var(--space-xs);
|
|
20302
|
+
position: relative;
|
|
20303
|
+
display: block;
|
|
20304
|
+
height: 100%;
|
|
20305
|
+
min-height: 2em;
|
|
20306
|
+
overflow: hidden;
|
|
20307
|
+
user-select: none;
|
|
20308
|
+
background: var(--surface-3);
|
|
20309
|
+
color: var(--surface-3-text);
|
|
20310
|
+
border-radius: var(--radius-s);
|
|
20311
|
+
border: 1px solid var(--border);
|
|
20312
|
+
}
|
|
20313
|
+
:host(:focus-visible) {
|
|
20314
|
+
outline: 2px solid var(--accent);
|
|
20315
|
+
outline-offset: -2px;
|
|
20316
|
+
}
|
|
20317
|
+
.viewport {
|
|
20318
|
+
position: relative;
|
|
20319
|
+
height: 100%;
|
|
20320
|
+
overflow-y: auto;
|
|
20321
|
+
overflow-x: hidden;
|
|
20322
|
+
}
|
|
20323
|
+
:host([orientation="row"]) .viewport {
|
|
20324
|
+
overflow-x: auto;
|
|
20325
|
+
overflow-y: hidden;
|
|
20326
|
+
}
|
|
20327
|
+
.spacer {
|
|
20328
|
+
position: relative;
|
|
20329
|
+
width: 100%;
|
|
20330
|
+
}
|
|
20331
|
+
.cell {
|
|
20332
|
+
position: absolute;
|
|
20333
|
+
inset: 0 auto auto 0;
|
|
20334
|
+
box-sizing: border-box;
|
|
20335
|
+
display: flex;
|
|
20336
|
+
flex-direction: column;
|
|
20337
|
+
align-items: stretch;
|
|
20338
|
+
contain: layout style;
|
|
20339
|
+
border-radius: var(--grid-list-radius);
|
|
20340
|
+
}
|
|
20341
|
+
|
|
20342
|
+
/* The square that holds the slotted content. */
|
|
20343
|
+
.thumb {
|
|
20344
|
+
position: relative;
|
|
20345
|
+
flex: 1;
|
|
20346
|
+
min-height: 0;
|
|
20347
|
+
display: flex;
|
|
20348
|
+
align-items: stretch;
|
|
20349
|
+
box-sizing: border-box;
|
|
20350
|
+
overflow: hidden;
|
|
20351
|
+
border-radius: 0;
|
|
20352
|
+
background: transparent;
|
|
20353
|
+
border: 1px solid transparent;
|
|
20354
|
+
transition: background var(--transition), border-color var(--transition);
|
|
20355
|
+
}
|
|
20356
|
+
.thumb ::slotted(*) {
|
|
20357
|
+
flex: 1;
|
|
20358
|
+
min-width: 0;
|
|
20359
|
+
min-height: 0;
|
|
20360
|
+
width: 100%;
|
|
20361
|
+
display: flex;
|
|
20362
|
+
align-items: center;
|
|
20363
|
+
justify-content: center;
|
|
20364
|
+
overflow: hidden;
|
|
20365
|
+
}
|
|
20366
|
+
.thumb ::slotted(*:empty)::after {
|
|
20367
|
+
content: "Empty";
|
|
20368
|
+
color: var(--surface-muted-text);
|
|
20369
|
+
font-size: var(--text-s);
|
|
20370
|
+
}
|
|
20371
|
+
::slotted([hidden]) {
|
|
20372
|
+
display: none !important;
|
|
20373
|
+
}
|
|
20374
|
+
|
|
20375
|
+
/* Selection / cursor wash — sits over the slotted content so it reads
|
|
20376
|
+
on photos and arbitrary DOM alike. */
|
|
20377
|
+
.thumb::after {
|
|
20378
|
+
content: "";
|
|
20379
|
+
position: absolute;
|
|
20380
|
+
inset: 0;
|
|
20381
|
+
pointer-events: none;
|
|
20382
|
+
border-radius: inherit;
|
|
20383
|
+
background: var(--accent);
|
|
20384
|
+
opacity: 0;
|
|
20385
|
+
transition: opacity var(--transition, 0.1s ease-out);
|
|
20386
|
+
}
|
|
20387
|
+
.cell.selected .thumb::after {
|
|
20388
|
+
opacity: 0.18;
|
|
20389
|
+
}
|
|
20390
|
+
|
|
20391
|
+
/* ---- Caption (base) ---- */
|
|
20392
|
+
.caption {
|
|
20393
|
+
box-sizing: border-box;
|
|
20394
|
+
flex: none;
|
|
20395
|
+
padding: 3px 6px;
|
|
20396
|
+
font-size: var(--text-s, 0.8rem);
|
|
20397
|
+
line-height: 1.3;
|
|
20398
|
+
text-align: center;
|
|
20399
|
+
white-space: nowrap;
|
|
20400
|
+
overflow: hidden;
|
|
20401
|
+
text-overflow: ellipsis;
|
|
20402
|
+
color: var(--surface-3-text);
|
|
20403
|
+
}
|
|
20404
|
+
:host([caption-align="left"]) .caption { text-align: left; }
|
|
20405
|
+
:host([caption-align="right"]) .caption { text-align: right; }
|
|
20406
|
+
|
|
20407
|
+
/* ---- Box modes (hidden / under / overlaid): the cell is one box with a
|
|
20408
|
+
single selection outline ---- */
|
|
20409
|
+
:host(:not([caption-style="separate"])) .cell {
|
|
20410
|
+
background: var(--grid-list-item-bg);
|
|
20411
|
+
border: 1px solid transparent;
|
|
20412
|
+
overflow: hidden;
|
|
20413
|
+
transition: background var(--transition), border-color var(--transition);
|
|
20414
|
+
}
|
|
20415
|
+
:host(:not([caption-style="separate"])) .cell:hover,
|
|
20416
|
+
:host(:not([caption-style="separate"])) .cell.tickled {
|
|
20417
|
+
background: var(--item-bg-tickled);
|
|
20418
|
+
border: var(--item-border-tickled);
|
|
20419
|
+
}
|
|
20420
|
+
:host(:not([caption-style="separate"])) .cell.selected {
|
|
20421
|
+
outline: 2px solid var(--accent);
|
|
20422
|
+
outline-offset: -2px;
|
|
20423
|
+
}
|
|
20424
|
+
:host(:not([caption-style="separate"])) .cell.selected.tickled {
|
|
20425
|
+
background: var(--item-bg-selected-tickled);
|
|
20426
|
+
outline: 2px dashed var(--surface-3);
|
|
20427
|
+
outline-offset: -2px;
|
|
20428
|
+
}
|
|
20429
|
+
|
|
20430
|
+
/* under: caption is a strip inside the box, below the content */
|
|
20431
|
+
:host([caption-style="under"]) .caption {
|
|
20432
|
+
border-top: 1px solid var(--border, rgba(128, 128, 128, 0.3));
|
|
20433
|
+
background: var(--surface-4, transparent);
|
|
20434
|
+
}
|
|
20435
|
+
|
|
20436
|
+
/* overlaid: caption floats over the content */
|
|
20437
|
+
:host([caption-style="overlaid"]) .caption {
|
|
20438
|
+
position: absolute;
|
|
20439
|
+
left: 0;
|
|
20440
|
+
right: 0;
|
|
20441
|
+
bottom: 0;
|
|
20442
|
+
color: #fff;
|
|
20443
|
+
}
|
|
20444
|
+
:host([caption-style="overlaid"][caption-position="top"]) .caption {
|
|
20445
|
+
top: 0;
|
|
20446
|
+
bottom: auto;
|
|
20447
|
+
}
|
|
20448
|
+
:host([caption-style="overlaid"][caption-position="middle"]) .caption {
|
|
20449
|
+
top: 50%;
|
|
20450
|
+
bottom: auto;
|
|
20451
|
+
transform: translateY(-50%);
|
|
20452
|
+
}
|
|
20453
|
+
/* overlaid backdrop */
|
|
20454
|
+
:host([caption-style="overlaid"][caption-backdrop="scrim"]) .caption {
|
|
20455
|
+
background: rgba(0, 0, 0, 0.55);
|
|
20456
|
+
}
|
|
20457
|
+
:host([caption-style="overlaid"][caption-backdrop="gradient"]) .caption {
|
|
20458
|
+
background: linear-gradient(to top, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
|
|
20459
|
+
padding-block: 10px 4px;
|
|
20460
|
+
}
|
|
20461
|
+
:host([caption-style="overlaid"][caption-backdrop="gradient"][caption-position="top"]) .caption {
|
|
20462
|
+
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0));
|
|
20463
|
+
padding-block: 4px 10px;
|
|
20464
|
+
}
|
|
20465
|
+
:host([caption-style="overlaid"][caption-backdrop="gradient"][caption-position="middle"]) .caption {
|
|
20466
|
+
background: rgba(0, 0, 0, 0.5);
|
|
20467
|
+
padding-block: 3px;
|
|
20468
|
+
}
|
|
20469
|
+
:host([caption-style="overlaid"][caption-backdrop="blur"]) .caption {
|
|
20470
|
+
background: rgba(20, 20, 20, 0.3);
|
|
20471
|
+
backdrop-filter: blur(6px);
|
|
20472
|
+
-webkit-backdrop-filter: blur(6px);
|
|
20473
|
+
}
|
|
20474
|
+
:host([caption-style="overlaid"][caption-backdrop="none"]) .caption {
|
|
20475
|
+
background: none;
|
|
20476
|
+
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.95), 0 0 6px rgba(0, 0, 0, 0.7);
|
|
20477
|
+
}
|
|
20478
|
+
|
|
20479
|
+
/* ---- separate mode: square + detached caption, each highlighted ---- */
|
|
20480
|
+
:host([caption-style="separate"]) .thumb {
|
|
20481
|
+
flex: none;
|
|
20482
|
+
border-radius: var(--grid-list-radius);
|
|
20483
|
+
background: var(--grid-list-item-bg);
|
|
20484
|
+
border: 1px solid transparent;
|
|
20485
|
+
}
|
|
20486
|
+
:host([caption-style="separate"]) .cell:hover .thumb,
|
|
20487
|
+
:host([caption-style="separate"]) .cell.tickled .thumb {
|
|
20488
|
+
background: var(--item-bg-tickled);
|
|
20489
|
+
border: var(--item-border-tickled);
|
|
20490
|
+
}
|
|
20491
|
+
:host([caption-style="separate"]) .caption {
|
|
20492
|
+
display: flex;
|
|
20493
|
+
align-items: center;
|
|
20494
|
+
align-self: center;
|
|
20495
|
+
max-width: 100%;
|
|
20496
|
+
margin-top: 4px;
|
|
20497
|
+
border-radius: var(--radius-s, 4px);
|
|
20498
|
+
}
|
|
20499
|
+
:host([caption-style="separate"][caption-align="left"]) .caption { align-self: flex-start; }
|
|
20500
|
+
:host([caption-style="separate"][caption-align="right"]) .caption { align-self: flex-end; }
|
|
20501
|
+
:host([caption-style="separate"]) .cell.selected .thumb {
|
|
20502
|
+
outline: 2px solid var(--accent);
|
|
20503
|
+
outline-offset: 2px;
|
|
20504
|
+
}
|
|
20505
|
+
:host([caption-style="separate"]) .cell.selected.tickled .thumb {
|
|
20506
|
+
background: var(--item-bg-selected-tickled);
|
|
20507
|
+
outline: 2px dashed var(--surface-3);
|
|
20508
|
+
outline-offset: 2px;
|
|
20509
|
+
}
|
|
20510
|
+
:host([caption-style="separate"]) .cell.selected .caption {
|
|
20511
|
+
background: var(--accent);
|
|
20512
|
+
color: var(--accent-text, #000);
|
|
20513
|
+
}
|
|
20514
|
+
:host([caption-style="separate"]) .cell.tickled .caption {
|
|
20515
|
+
background: var(--item-bg-tickled);
|
|
20516
|
+
outline: var(--item-border-tickled);
|
|
20517
|
+
}
|
|
20518
|
+
:host([caption-style="separate"]) .cell.selected.tickled .caption {
|
|
20519
|
+
background: var(--item-bg-selected-tickled);
|
|
20520
|
+
outline: 2px dashed var(--surface-3);
|
|
20521
|
+
outline-offset: -2px;
|
|
20522
|
+
}
|
|
20523
|
+
|
|
20524
|
+
/* ---- Insertion / removal animation ----
|
|
20525
|
+
Active only while the host carries [reflowing] — the ~0.3s window after
|
|
20526
|
+
the child list changes mid-list (see #noteChildMutation). Kept out of
|
|
20527
|
+
the base rules so it never fires during scroll virtualisation, where
|
|
20528
|
+
cells are recycled to wildly different positions every frame. Placed
|
|
20529
|
+
last so it wins over the earlier cell transition rule regardless of
|
|
20530
|
+
specificity. Survivors slide to their new slot; a freshly inserted cell
|
|
20531
|
+
also fades in. (A removed item's element is already gone, so there's
|
|
20532
|
+
nothing left to fade — the gap simply closes as the others slide.) */
|
|
20533
|
+
:host([reflowing]) .cell {
|
|
20534
|
+
transition:
|
|
20535
|
+
transform var(--grid-list-item-move-duration, 200ms) cubic-bezier(0.2, 0, 0, 1),
|
|
20536
|
+
background var(--transition),
|
|
20537
|
+
border-color var(--transition);
|
|
20538
|
+
}
|
|
20539
|
+
.cell.entering {
|
|
20540
|
+
animation: ixfx-list-item-in var(--grid-list-item-move-duration, 200ms) ease both;
|
|
20541
|
+
}
|
|
20542
|
+
@media (prefers-reduced-motion: reduce) {
|
|
20543
|
+
:host([reflowing]) .cell { transition: background var(--transition), border-color var(--transition); }
|
|
20544
|
+
.cell.entering { animation: none; }
|
|
20545
|
+
}
|
|
20546
|
+
`
|
|
20547
|
+
];
|
|
20548
|
+
}
|
|
20549
|
+
#viewportRef;
|
|
20550
|
+
#resizeObserver;
|
|
20551
|
+
#mutationObserver;
|
|
20552
|
+
#scrollRaf;
|
|
20553
|
+
#items;
|
|
20554
|
+
#itemWrap;
|
|
20555
|
+
/** Items added mid-list that are still playing their enter animation. */
|
|
20556
|
+
#entering;
|
|
20557
|
+
/** Timer that ends the `[reflowing]` window opened by a mid-list insertion. */
|
|
20558
|
+
#reflowTimer;
|
|
20559
|
+
/**
|
|
20560
|
+
* When cell geometry changes (zoom), the item nearest the viewport centre is
|
|
20561
|
+
* captured here in `willUpdate` and the scroll position is corrected in
|
|
20562
|
+
* `updated` so that item stays put — rather than the grid anchoring to its
|
|
20563
|
+
* top-left corner.
|
|
20564
|
+
*/
|
|
20565
|
+
#zoomAnchor;
|
|
20566
|
+
/**
|
|
20567
|
+
* Per-item slot name. Each cell renders its own named `<slot>` and the
|
|
20568
|
+
* matching light-DOM child carries `slot="<name>"`, so a child is projected
|
|
20569
|
+
* into exactly one cell. (A single unnamed slot would collapse every child
|
|
20570
|
+
* into the first cell.)
|
|
20571
|
+
*/
|
|
20572
|
+
#slotNames;
|
|
20573
|
+
#slotSeq;
|
|
20574
|
+
#searchRegistry;
|
|
20575
|
+
#unregisterSearchCommands;
|
|
20576
|
+
/** Command registry carrying this grid's `list.search.*` commands. */
|
|
20577
|
+
get registry() {
|
|
20578
|
+
return this.#searchRegistry;
|
|
20579
|
+
}
|
|
20580
|
+
/** Effective search modes: this component's capability (all three) ∩ the `search-modes` attribute. */
|
|
20581
|
+
#effectiveSearchModes() {
|
|
20582
|
+
const raw = this.searchModes?.trim();
|
|
20583
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
20584
|
+
const requested = new Set(raw.split(/\s+/));
|
|
20585
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
20586
|
+
}
|
|
20587
|
+
#syncSearchCommands() {
|
|
20588
|
+
if (!this.#searchRegistry) return;
|
|
20589
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
20590
|
+
this.#unregisterSearchCommands?.();
|
|
20591
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
20592
|
+
}
|
|
20593
|
+
connectedCallback() {
|
|
20594
|
+
super.connectedCallback();
|
|
20595
|
+
if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
|
|
20596
|
+
for (const [attr, value] of [
|
|
20597
|
+
[`caption-style`, this.captionStyle],
|
|
20598
|
+
[`caption-align`, this.captionAlign],
|
|
20599
|
+
[`caption-position`, this.captionPosition],
|
|
20600
|
+
[`caption-backdrop`, this.captionBackdrop]
|
|
20601
|
+
]) if (!this.hasAttribute(attr)) this.setAttribute(attr, value);
|
|
20602
|
+
this.addEventListener(`keydown`, this._onKeyDown);
|
|
20603
|
+
this.#mutationObserver ??= new MutationObserver((records) => this.#onChildrenChanged(records));
|
|
20604
|
+
this.#mutationObserver.observe(this, {
|
|
20605
|
+
childList: true,
|
|
20606
|
+
attributeFilter: [`hidden`]
|
|
20607
|
+
});
|
|
20608
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
20609
|
+
this.#syncSearchCommands();
|
|
20610
|
+
}
|
|
20611
|
+
disconnectedCallback() {
|
|
20612
|
+
super.disconnectedCallback();
|
|
20613
|
+
this.removeEventListener(`keydown`, this._onKeyDown);
|
|
20614
|
+
this.#resizeObserver?.disconnect();
|
|
20615
|
+
this.#resizeObserver = void 0;
|
|
20616
|
+
this.#mutationObserver?.disconnect();
|
|
20617
|
+
this.#cancelScrollRaf();
|
|
20618
|
+
this.#unregisterSearchCommands?.();
|
|
20619
|
+
this.#unregisterSearchCommands = void 0;
|
|
20620
|
+
this.#searchRegistry = void 0;
|
|
20621
|
+
if (this.#reflowTimer) {
|
|
20622
|
+
clearTimeout(this.#reflowTimer);
|
|
20623
|
+
this.#reflowTimer = 0;
|
|
20624
|
+
}
|
|
20625
|
+
}
|
|
20626
|
+
#onChildrenChanged(records) {
|
|
20627
|
+
const previousLength = this.#items.length;
|
|
20628
|
+
this.#syncItems();
|
|
20629
|
+
this._syncItemStates();
|
|
20630
|
+
this.#noteChildMutation(previousLength, records);
|
|
20631
|
+
this.requestUpdate();
|
|
20632
|
+
this.updateComplete.then(() => this.#emitViewportChange());
|
|
20633
|
+
}
|
|
20634
|
+
/**
|
|
20635
|
+
* When the child list changes mid-list — not a first populate or a wholesale
|
|
20636
|
+
* replace — mark any newcomers for an enter animation and open a short
|
|
20637
|
+
* `[reflowing]` window so the cells that shift slide to their new position
|
|
20638
|
+
* instead of snapping. Removals get the same window (the survivors slide to
|
|
20639
|
+
* close the gap). The window is bounded in time so the transform transition
|
|
20640
|
+
* never bleeds into scroll virtualisation.
|
|
20641
|
+
*
|
|
20642
|
+
* Reads the `MutationRecord`s directly rather than diffing `#items`, because a
|
|
20643
|
+
* `requestUpdate()` between the DOM change and this callback (e.g. a caller
|
|
20644
|
+
* that clears the selection before removing) lets `updated()` re-sync `#items`
|
|
20645
|
+
* first, which would hide the change from a naive diff.
|
|
20646
|
+
*/
|
|
20647
|
+
#noteChildMutation(previousLength, records) {
|
|
20648
|
+
if (!this.hasUpdated || !records || records.length === 0) return;
|
|
20649
|
+
const currentSet = new Set(this.#items);
|
|
20650
|
+
const added = [];
|
|
20651
|
+
let removedCount = 0;
|
|
20652
|
+
for (const record of records) {
|
|
20653
|
+
for (const node of record.addedNodes) if (node.nodeType === Node.ELEMENT_NODE && currentSet.has(node)) added.push(node);
|
|
20654
|
+
for (const node of record.removedNodes) if (node.nodeType === Node.ELEMENT_NODE) removedCount++;
|
|
20655
|
+
}
|
|
20656
|
+
if (added.length === 0 && removedCount === 0) return;
|
|
20657
|
+
if (previousLength === 0 && removedCount === 0) return;
|
|
20658
|
+
if (added.length > 0 && added.length === this.#items.length) return;
|
|
20659
|
+
for (const el of added) this.#entering.add(el);
|
|
20660
|
+
this.toggleAttribute(`reflowing`, true);
|
|
20661
|
+
if (this.#reflowTimer) clearTimeout(this.#reflowTimer);
|
|
20662
|
+
this.#reflowTimer = window.setTimeout(() => {
|
|
20663
|
+
this.#reflowTimer = 0;
|
|
20664
|
+
this.#entering.clear();
|
|
20665
|
+
this.toggleAttribute(`reflowing`, false);
|
|
20666
|
+
this.requestUpdate();
|
|
20667
|
+
}, REFLOW_WINDOW_MS);
|
|
20668
|
+
}
|
|
20669
|
+
#emitViewportChange() {
|
|
20670
|
+
this._dispatch(`grid-list-viewport-change`, { visibleKeys: this.visibleKeys() });
|
|
20671
|
+
}
|
|
20672
|
+
firstUpdated() {
|
|
20673
|
+
const el = this.#viewportRef.value;
|
|
20674
|
+
if (!el) return;
|
|
20675
|
+
this._viewportWidth = el.clientWidth;
|
|
20676
|
+
this._viewportHeight = el.clientHeight;
|
|
20677
|
+
this.#resizeObserver = new ResizeObserver((entries) => {
|
|
20678
|
+
for (const entry of entries) {
|
|
20679
|
+
this._viewportWidth = entry.contentRect.width;
|
|
20680
|
+
this._viewportHeight = entry.contentRect.height;
|
|
20681
|
+
}
|
|
20682
|
+
});
|
|
20683
|
+
this.#resizeObserver.observe(el);
|
|
20684
|
+
this.#syncItems();
|
|
20685
|
+
this.requestUpdate();
|
|
20686
|
+
}
|
|
20687
|
+
willUpdate(changed) {
|
|
20688
|
+
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
20689
|
+
if (changed.has(`filterPredicate`)) this.#syncItems();
|
|
20690
|
+
if (changed.has(`itemSize`) || changed.has(`gap`)) {
|
|
20691
|
+
this.itemSize = Math.max(MIN_ITEM_SIZE, Math.round(this.itemSize));
|
|
20692
|
+
this.gap = Math.max(0, Math.round(this.gap));
|
|
20693
|
+
}
|
|
20694
|
+
if (changed.has(`captionHeight`)) this.captionHeight = Math.max(0, Math.round(this.captionHeight));
|
|
20695
|
+
if (changed.has(`itemWrap`)) this.#itemWrap = this.itemWrap;
|
|
20696
|
+
if ((changed.has(`itemSize`) || changed.has(`gap`) || changed.has(`captionStyle`) || changed.has(`captionHeight`) || changed.has(`orientation`)) && this.hasUpdated && this.#items.length > 0) {
|
|
20697
|
+
const prevOrientation = changed.get(`orientation`) ?? this.orientation;
|
|
20698
|
+
const horizontal = prevOrientation === `row`;
|
|
20699
|
+
const prevOffset = horizontal ? this._scrollLeft : this._scrollTop;
|
|
20700
|
+
const prevExtent = horizontal ? this._viewportWidth : this._viewportHeight;
|
|
20701
|
+
if (prevExtent > 0 && prevOffset > 1) {
|
|
20702
|
+
const prevItemSize = changed.get(`itemSize`) ?? this.itemSize;
|
|
20703
|
+
const prevGap = changed.get(`gap`) ?? this.gap;
|
|
20704
|
+
const prevCaptionStyle = changed.get(`captionStyle`) ?? this.captionStyle;
|
|
20705
|
+
const prevCaptionHeight = changed.get(`captionHeight`) ?? this.captionHeight;
|
|
20706
|
+
const prevRowHeight = prevItemSize + (prevCaptionStyle === `separate` || prevCaptionStyle === `under` ? prevCaptionHeight : 0);
|
|
20707
|
+
const prev = computeGridWindow(this.#items.length, prevItemSize, prevRowHeight, prevGap, this._viewportWidth, this._viewportHeight, prevOffset, prevOrientation);
|
|
20708
|
+
const mainStride = horizontal ? prev.colStride : prev.rowStride;
|
|
20709
|
+
const centerLine = (prevOffset + prevExtent / 2) / mainStride;
|
|
20710
|
+
const lineFloor = Math.max(0, Math.floor(centerLine));
|
|
20711
|
+
const index = horizontal ? Math.min(this.#items.length - 1, lineFloor) : Math.min(this.#items.length - 1, lineFloor * prev.cols + Math.max(0, Math.floor((prev.cols - 1) / 2)));
|
|
20712
|
+
this.#zoomAnchor = {
|
|
20713
|
+
index,
|
|
20714
|
+
lineFrac: centerLine - lineFloor
|
|
20715
|
+
};
|
|
20716
|
+
}
|
|
20717
|
+
}
|
|
20718
|
+
}
|
|
20719
|
+
/** Full cell height: the square, plus a caption band in `separate` / `under`. */
|
|
20720
|
+
#rowHeight() {
|
|
20721
|
+
const captionBand = this.captionStyle === `separate` || this.captionStyle === `under`;
|
|
20722
|
+
return this.itemSize + (captionBand ? this.captionHeight : 0);
|
|
20723
|
+
}
|
|
20724
|
+
#captionFor(el) {
|
|
20725
|
+
return el.getAttribute(`data-label`) ?? el.querySelector(`[data-grid-list-caption]`)?.textContent?.trim() ?? ``;
|
|
20726
|
+
}
|
|
20727
|
+
updated(changed) {
|
|
20728
|
+
super.updated(changed);
|
|
20729
|
+
this.#syncItems();
|
|
20730
|
+
if (changed.has(`selectionMode`) || changed.has(`interactionMode`)) this.selection.clearAnchor();
|
|
20731
|
+
if (changed.has(`orientation`)) {
|
|
20732
|
+
const vp = this.#viewportRef.value;
|
|
20733
|
+
if (vp) {
|
|
20734
|
+
if (this.orientation === `row`) {
|
|
20735
|
+
vp.scrollTop = 0;
|
|
20736
|
+
this._scrollTop = 0;
|
|
20737
|
+
} else {
|
|
20738
|
+
vp.scrollLeft = 0;
|
|
20739
|
+
this._scrollLeft = 0;
|
|
20740
|
+
}
|
|
20741
|
+
}
|
|
20742
|
+
}
|
|
20743
|
+
this.#applyZoomAnchor();
|
|
20744
|
+
}
|
|
20745
|
+
/** Re-anchor the scroll so the item captured in `willUpdate` stays centred. */
|
|
20746
|
+
#applyZoomAnchor() {
|
|
20747
|
+
if (!this.#zoomAnchor) return;
|
|
20748
|
+
const { index, lineFrac } = this.#zoomAnchor;
|
|
20749
|
+
this.#zoomAnchor = void 0;
|
|
20750
|
+
const vp = this.#viewportRef.value;
|
|
20751
|
+
if (!vp) return;
|
|
20752
|
+
const win = this.#currentWindow();
|
|
20753
|
+
if (win.horizontal) {
|
|
20754
|
+
if (this._viewportWidth <= 0) return;
|
|
20755
|
+
const targetCenter = (index + lineFrac) * win.colStride;
|
|
20756
|
+
const maxScroll = Math.max(0, win.totalWidth - this._viewportWidth);
|
|
20757
|
+
const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportWidth / 2));
|
|
20758
|
+
if (Math.abs(vp.scrollLeft - next) > .5) {
|
|
20759
|
+
vp.scrollLeft = next;
|
|
20760
|
+
this._scrollLeft = next;
|
|
20761
|
+
}
|
|
20762
|
+
return;
|
|
20763
|
+
}
|
|
20764
|
+
if (this._viewportHeight <= 0) return;
|
|
20765
|
+
const targetCenter = ((win.cols > 0 ? Math.floor(index / win.cols) : 0) + lineFrac) * win.rowStride;
|
|
20766
|
+
const maxScroll = Math.max(0, win.totalHeight - this._viewportHeight);
|
|
20767
|
+
const next = Math.min(maxScroll, Math.max(0, targetCenter - this._viewportHeight / 2));
|
|
20768
|
+
if (Math.abs(vp.scrollTop - next) > .5) {
|
|
20769
|
+
vp.scrollTop = next;
|
|
20770
|
+
this._scrollTop = next;
|
|
20771
|
+
}
|
|
20772
|
+
}
|
|
20773
|
+
#syncItems() {
|
|
20774
|
+
this.#items = Array.from(this.querySelectorAll(ITEM_SELECTOR$1)).filter((el) => !el.hidden && (this.filterPredicate === void 0 || this.filterPredicate(el)));
|
|
20775
|
+
for (const el of this.#items) {
|
|
20776
|
+
const name = this.#slotNameFor(el);
|
|
20777
|
+
if (el.getAttribute(`slot`) !== name) el.setAttribute(`slot`, name);
|
|
20778
|
+
}
|
|
20779
|
+
}
|
|
20780
|
+
#slotNameFor(el) {
|
|
20781
|
+
let name = this.#slotNames.get(el);
|
|
20782
|
+
if (name === void 0) {
|
|
20783
|
+
this.#slotSeq += 1;
|
|
20784
|
+
name = `gl-item-${this.#slotSeq}`;
|
|
20785
|
+
this.#slotNames.set(el, name);
|
|
20786
|
+
}
|
|
20787
|
+
return name;
|
|
20788
|
+
}
|
|
20789
|
+
_getVisibleItems() {
|
|
20790
|
+
return this.#items.filter((el) => {
|
|
20791
|
+
if (el.hidden) return false;
|
|
20792
|
+
return true;
|
|
20793
|
+
});
|
|
20794
|
+
}
|
|
20795
|
+
get count() {
|
|
20796
|
+
return this.#items.length;
|
|
20797
|
+
}
|
|
20798
|
+
/**
|
|
20799
|
+
* Number of cells currently rendered in the shadow DOM. With virtualization
|
|
20800
|
+
* this is roughly the visible rows plus overscan, regardless of how large
|
|
20801
|
+
* `count` is — useful for demos/diagnostics.
|
|
20802
|
+
*/
|
|
20803
|
+
get renderedCount() {
|
|
20804
|
+
return this.renderRoot.querySelectorAll(`.cell`).length;
|
|
20805
|
+
}
|
|
20806
|
+
get selectedItems() {
|
|
20807
|
+
return this.selection.selected;
|
|
20808
|
+
}
|
|
20809
|
+
/** The selection as stable `data-key` / `data-value` strings. */
|
|
20810
|
+
get selectedKeys() {
|
|
20811
|
+
return this.selection.selectedKeys;
|
|
20812
|
+
}
|
|
20813
|
+
select(item) {
|
|
20814
|
+
this.selection.replace(item);
|
|
20815
|
+
}
|
|
20816
|
+
selectMany(items) {
|
|
20817
|
+
this.selection.selectMany(items);
|
|
20818
|
+
}
|
|
20819
|
+
deselect(item) {
|
|
20820
|
+
this.selection.deselect(item);
|
|
20821
|
+
}
|
|
20822
|
+
clearSelection() {
|
|
20823
|
+
this.selection.clear();
|
|
20824
|
+
}
|
|
20825
|
+
selectAll() {
|
|
20826
|
+
if (this.selection.canSelectAll()) this.selection.selectAll();
|
|
20827
|
+
}
|
|
20828
|
+
/**
|
|
20829
|
+
* Replace the selection with the items carrying these keys. Unknown keys are
|
|
20830
|
+
* ignored; survives the host rebuilding the item set. Requires each item to
|
|
20831
|
+
* have a `data-key` (or `data-value`).
|
|
20832
|
+
*/
|
|
20833
|
+
selectKeys(keys) {
|
|
20834
|
+
this.selection.selectKeys(keys, (key) => this.getItem(key));
|
|
20835
|
+
}
|
|
20836
|
+
/** Add items by key to the selection (multi-select modes only). */
|
|
20837
|
+
addSelectedKeys(keys) {
|
|
20838
|
+
this.selection.addKeys(keys, (key) => this.getItem(key));
|
|
20839
|
+
}
|
|
20840
|
+
/** Remove items by key from the selection. */
|
|
20841
|
+
removeSelectedKeys(keys) {
|
|
20842
|
+
this.selection.removeKeys(keys, (key) => this.getItem(key));
|
|
20843
|
+
}
|
|
20844
|
+
/** Move the cursor to the item with this key and scroll it into view. */
|
|
20845
|
+
focusKey(key) {
|
|
20846
|
+
const el = this.getItem(key);
|
|
20847
|
+
if (!el) return;
|
|
20848
|
+
this.tickled.focus(el);
|
|
20849
|
+
this.#scrollItemIntoView(el);
|
|
20850
|
+
}
|
|
20851
|
+
/** The key of the item under the cursor, or `undefined`. */
|
|
20852
|
+
get tickledKey() {
|
|
20853
|
+
const id = this.tickled.tickledId;
|
|
20854
|
+
return id ? keyForElement(id) : void 0;
|
|
20855
|
+
}
|
|
20856
|
+
getItem(key) {
|
|
20857
|
+
return this.#items.find((el) => keyForElement(el) === key);
|
|
20858
|
+
}
|
|
20859
|
+
getItemByIndex(index) {
|
|
20860
|
+
return this.#items[index];
|
|
20861
|
+
}
|
|
20862
|
+
getKey(item) {
|
|
20863
|
+
return keyForElement(item);
|
|
20864
|
+
}
|
|
20865
|
+
visibleKeys() {
|
|
20866
|
+
const win = this.#currentWindow();
|
|
20867
|
+
return this.#items.slice(win.startIndex, win.endIndex).map((el) => keyForElement(el)).filter((k) => k !== void 0);
|
|
20868
|
+
}
|
|
20869
|
+
/** Adjust the scroll offset so `el` is fully within the viewport. */
|
|
20870
|
+
#scrollItemIntoView(el) {
|
|
20871
|
+
const index = this.#items.indexOf(el);
|
|
20872
|
+
const vp = this.#viewportRef.value;
|
|
20873
|
+
if (index < 0 || !vp) return;
|
|
20874
|
+
const win = this.#currentWindow();
|
|
20875
|
+
if (win.horizontal) {
|
|
20876
|
+
const left = index * win.colStride;
|
|
20877
|
+
const right = left + this.itemSize;
|
|
20878
|
+
const viewLeft = vp.scrollLeft;
|
|
20879
|
+
let next = viewLeft;
|
|
20880
|
+
if (left < viewLeft) next = left;
|
|
20881
|
+
else if (right > viewLeft + this._viewportWidth) next = right - this._viewportWidth;
|
|
20882
|
+
if (next !== viewLeft) {
|
|
20883
|
+
vp.scrollLeft = next;
|
|
20884
|
+
this._scrollLeft = next;
|
|
20885
|
+
}
|
|
20886
|
+
return;
|
|
20887
|
+
}
|
|
20888
|
+
const top = (win.cols > 0 ? Math.floor(index / win.cols) : 0) * win.rowStride;
|
|
20889
|
+
const bottom = top + this.#rowHeight();
|
|
20890
|
+
const viewTop = vp.scrollTop;
|
|
20891
|
+
let next = viewTop;
|
|
20892
|
+
if (top < viewTop) next = top;
|
|
20893
|
+
else if (bottom > viewTop + this._viewportHeight) next = bottom - this._viewportHeight;
|
|
20894
|
+
if (next !== viewTop) {
|
|
20895
|
+
vp.scrollTop = next;
|
|
20896
|
+
this._scrollTop = next;
|
|
20897
|
+
}
|
|
20898
|
+
}
|
|
20899
|
+
_syncItemStates() {
|
|
20900
|
+
for (const el of this.#items) {
|
|
20901
|
+
el.toggleAttribute(`data-tickled`, this.tickled.isTickled(el));
|
|
20902
|
+
el.toggleAttribute(`data-selected`, this.selection.selected.has(el));
|
|
20903
|
+
}
|
|
20904
|
+
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
20905
|
+
}
|
|
20906
|
+
_dispatch(name, detail) {
|
|
20907
|
+
this.dispatchEvent(new CustomEvent(name, {
|
|
20908
|
+
detail,
|
|
20909
|
+
bubbles: true,
|
|
20910
|
+
composed: true
|
|
20911
|
+
}));
|
|
20912
|
+
}
|
|
20913
|
+
render() {
|
|
20914
|
+
const win = this.#currentWindow();
|
|
20915
|
+
const slice = this.#items.slice(win.startIndex, win.endIndex);
|
|
20916
|
+
const spacerStyle = win.horizontal ? `width:${win.totalWidth}px;height:100%` : `height:${win.totalHeight}px`;
|
|
20917
|
+
const scrollFade = win.horizontal ? `scroll-fade-x` : `scroll-fade-y`;
|
|
20918
|
+
return b`
|
|
20919
|
+
<div
|
|
20920
|
+
class="viewport ${scrollFade}"
|
|
20921
|
+
part="viewport"
|
|
20922
|
+
${ref(this.#viewportRef)}
|
|
20923
|
+
data-input-mode=${this.tickled.inputMode}
|
|
20924
|
+
@scroll=${this.#onScroll}
|
|
20925
|
+
>
|
|
20926
|
+
<div class="spacer" style=${spacerStyle}>
|
|
20927
|
+
${repeat(slice, (el) => this.#slotNameFor(el), (el, i) => this.#renderCell(el, win.startIndex + i, win))}
|
|
20928
|
+
</div>
|
|
20929
|
+
</div>
|
|
20930
|
+
${this.search.renderField()}
|
|
20931
|
+
`;
|
|
20932
|
+
}
|
|
20933
|
+
#renderCell(el, index, win) {
|
|
20934
|
+
const selected = this.selection.selected.has(el);
|
|
20935
|
+
const tickled = this.tickled.isTickled(el);
|
|
20936
|
+
const entering = this.#entering.has(el);
|
|
20937
|
+
const style = this.captionStyle;
|
|
20938
|
+
const separate = style === `separate`;
|
|
20939
|
+
const caption = style === `hidden` ? `` : this.#captionFor(el);
|
|
20940
|
+
const showCaption = caption !== ``;
|
|
20941
|
+
const cellHeight = win.rowStride - this.gap;
|
|
20942
|
+
const x = win.horizontal ? index * win.colStride : columnOf(index, win.cols) * win.colStride;
|
|
20943
|
+
const y = win.horizontal ? Math.max(0, (this._viewportHeight - cellHeight) / 2) : rowOf(index, win.cols) * win.rowStride;
|
|
20944
|
+
const thumbStyle = separate ? `height:${this.itemSize}px` : ``;
|
|
20945
|
+
const captionCss = separate ? `height:${Math.max(0, this.captionHeight - 4)}px` : style === `under` ? `height:${this.captionHeight}px` : ``;
|
|
20946
|
+
return b`
|
|
20947
|
+
<div
|
|
20948
|
+
class="cell${selected ? ` selected` : ``}${tickled ? ` tickled` : ``}${entering ? ` entering` : ``}"
|
|
20949
|
+
part="item"
|
|
20950
|
+
style="width:${this.itemSize}px;height:${cellHeight}px;transform:translate(${x}px,${y}px)"
|
|
20951
|
+
data-index=${index}
|
|
20952
|
+
@click=${(e) => this.#onCellClick(e, el)}
|
|
20953
|
+
@dblclick=${() => this.#onCellActivate(el)}
|
|
20954
|
+
@pointerenter=${() => this.tickled.handlePointerEnter(el)}
|
|
20955
|
+
>
|
|
20956
|
+
<div class="thumb" part="thumb" style=${thumbStyle}>
|
|
20957
|
+
<slot name=${this.#slotNameFor(el)}></slot>
|
|
20958
|
+
</div>
|
|
20959
|
+
${showCaption ? b`<div class="caption" part="caption" style=${captionCss}>${caption}</div>` : ``}
|
|
20960
|
+
</div>
|
|
20961
|
+
`;
|
|
20962
|
+
}
|
|
20963
|
+
#onScroll;
|
|
20964
|
+
#cancelScrollRaf() {
|
|
20965
|
+
if (this.#scrollRaf !== 0) {
|
|
20966
|
+
cancelAnimationFrame(this.#scrollRaf);
|
|
20967
|
+
this.#scrollRaf = 0;
|
|
20968
|
+
}
|
|
20969
|
+
}
|
|
20970
|
+
#onCellClick(e, item) {
|
|
20971
|
+
this.selection.handleClick(item, e);
|
|
20972
|
+
this._dispatch(`list-item-click`, { item });
|
|
20973
|
+
}
|
|
20974
|
+
#onCellActivate(item) {
|
|
20975
|
+
if (this.interactionMode !== `manual`) this.selection.replace(item);
|
|
20976
|
+
this._dispatch(`list-activate`, { item });
|
|
20977
|
+
}
|
|
20978
|
+
#currentWindow() {
|
|
20979
|
+
return computeGridWindow(this.#items.length, this.itemSize, this.#rowHeight(), this.gap, this._viewportWidth, this._viewportHeight, this.orientation === `row` ? this._scrollLeft : this._scrollTop, this.orientation);
|
|
20980
|
+
}
|
|
20981
|
+
};
|
|
20982
|
+
__decorate([property({
|
|
20983
|
+
type: String,
|
|
20984
|
+
attribute: `selection-mode`
|
|
20985
|
+
})], GridListElement.prototype, "selectionMode", void 0);
|
|
20986
|
+
__decorate([property({
|
|
20987
|
+
type: String,
|
|
20988
|
+
attribute: `interaction-mode`
|
|
20989
|
+
})], GridListElement.prototype, "interactionMode", void 0);
|
|
20990
|
+
__decorate([property({
|
|
20991
|
+
type: Number,
|
|
20992
|
+
attribute: `item-size`
|
|
20993
|
+
})], GridListElement.prototype, "itemSize", void 0);
|
|
20994
|
+
__decorate([property({ type: Number })], GridListElement.prototype, "gap", void 0);
|
|
20995
|
+
__decorate([property({
|
|
20996
|
+
type: String,
|
|
20997
|
+
reflect: true
|
|
20998
|
+
})], GridListElement.prototype, "orientation", void 0);
|
|
20999
|
+
__decorate([property({
|
|
21000
|
+
type: String,
|
|
21001
|
+
attribute: `caption-style`,
|
|
21002
|
+
reflect: true
|
|
21003
|
+
})], GridListElement.prototype, "captionStyle", void 0);
|
|
21004
|
+
__decorate([property({
|
|
21005
|
+
type: String,
|
|
21006
|
+
attribute: `caption-align`,
|
|
21007
|
+
reflect: true
|
|
21008
|
+
})], GridListElement.prototype, "captionAlign", void 0);
|
|
21009
|
+
__decorate([property({
|
|
21010
|
+
type: String,
|
|
21011
|
+
attribute: `caption-position`,
|
|
21012
|
+
reflect: true
|
|
21013
|
+
})], GridListElement.prototype, "captionPosition", void 0);
|
|
21014
|
+
__decorate([property({
|
|
21015
|
+
type: String,
|
|
21016
|
+
attribute: `caption-backdrop`,
|
|
21017
|
+
reflect: true
|
|
21018
|
+
})], GridListElement.prototype, "captionBackdrop", void 0);
|
|
21019
|
+
__decorate([property({
|
|
21020
|
+
type: Number,
|
|
21021
|
+
attribute: `caption-height`
|
|
21022
|
+
})], GridListElement.prototype, "captionHeight", void 0);
|
|
21023
|
+
__decorate([property({
|
|
21024
|
+
type: String,
|
|
21025
|
+
attribute: `item-wrap`
|
|
21026
|
+
})], GridListElement.prototype, "itemWrap", void 0);
|
|
21027
|
+
__decorate([property({ attribute: false })], GridListElement.prototype, "filterPredicate", void 0);
|
|
21028
|
+
__decorate([property({
|
|
21029
|
+
type: String,
|
|
21030
|
+
attribute: `search-modes`,
|
|
21031
|
+
reflect: true
|
|
21032
|
+
})], GridListElement.prototype, "searchModes", void 0);
|
|
21033
|
+
__decorate([state()], GridListElement.prototype, "_scrollTop", void 0);
|
|
21034
|
+
__decorate([state()], GridListElement.prototype, "_scrollLeft", void 0);
|
|
21035
|
+
__decorate([state()], GridListElement.prototype, "_viewportHeight", void 0);
|
|
21036
|
+
__decorate([state()], GridListElement.prototype, "_viewportWidth", void 0);
|
|
21037
|
+
GridListElement = __decorate([safeCustomElement(`ixfx-grid-list`)], GridListElement);
|
|
21038
|
+
//#endregion
|
|
18704
21039
|
//#region src/panel/panel.ts
|
|
18705
21040
|
let PanelElement = class PanelElement extends i {
|
|
18706
21041
|
constructor(..._args) {
|
|
@@ -19053,6 +21388,8 @@ __decorate([property({
|
|
|
19053
21388
|
GroupedItemListerElement = __decorate([safeCustomElement(`ixfx-grouped-item-lister`)], GroupedItemListerElement);
|
|
19054
21389
|
//#endregion
|
|
19055
21390
|
//#region src/tree/tree-base.ts
|
|
21391
|
+
/** Above this many rows entering / leaving at once, skip the animation entirely. */
|
|
21392
|
+
const MAX_ANIMATED_ROWS = 50;
|
|
19056
21393
|
let TreeBaseElement = class TreeBaseElement extends i {
|
|
19057
21394
|
constructor(..._args) {
|
|
19058
21395
|
super(..._args);
|
|
@@ -19075,8 +21412,19 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19075
21412
|
}
|
|
19076
21413
|
});
|
|
19077
21414
|
this._focusedDepth = 0;
|
|
21415
|
+
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
21416
|
+
labelSelector: `.tree-label`,
|
|
21417
|
+
highlightKey: `incr-search-highlight`
|
|
21418
|
+
}) });
|
|
19078
21419
|
this.#config = { indent: 20 };
|
|
21420
|
+
this.#renderedKeys = [];
|
|
21421
|
+
this.#lastNodeByKey = /* @__PURE__ */ new Map();
|
|
21422
|
+
this.#enteringKeys = /* @__PURE__ */ new Set();
|
|
21423
|
+
this.#leaving = /* @__PURE__ */ new Map();
|
|
21424
|
+
this.#leavingAnimating = /* @__PURE__ */ new Set();
|
|
21425
|
+
this.#treeAnimReady = false;
|
|
19079
21426
|
this.#onKeyDown = (event) => {
|
|
21427
|
+
if (this.search.isOpen) return;
|
|
19080
21428
|
const nodes = this.#getAllNodes();
|
|
19081
21429
|
if (nodes.length === 0) return;
|
|
19082
21430
|
if ((event.ctrlKey || event.metaKey) && event.key === `a`) {
|
|
@@ -19137,7 +21485,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19137
21485
|
bubbles: true,
|
|
19138
21486
|
composed: true
|
|
19139
21487
|
}));
|
|
19140
|
-
} else if (current.depth >
|
|
21488
|
+
} else if (current.depth > 0) {
|
|
19141
21489
|
const parentDepth = current.depth - 1;
|
|
19142
21490
|
for (let i = currentIdx - 1; i >= 0; i--) if (nodes[i].depth === parentDepth) {
|
|
19143
21491
|
this.#setFocusedIndex(i);
|
|
@@ -19197,12 +21545,111 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19197
21545
|
get selectedNodes() {
|
|
19198
21546
|
return this._selectedNodes;
|
|
19199
21547
|
}
|
|
21548
|
+
#unregisterSearchCommands;
|
|
21549
|
+
#effectiveSearchModes() {
|
|
21550
|
+
const raw = this.searchModes?.trim();
|
|
21551
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
21552
|
+
const requested = new Set(raw.split(/\s+/));
|
|
21553
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
21554
|
+
}
|
|
21555
|
+
#syncSearchCommands() {
|
|
21556
|
+
if (!this.#ownedRegistry) return;
|
|
21557
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
21558
|
+
this.#unregisterSearchCommands?.();
|
|
21559
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
21560
|
+
}
|
|
19200
21561
|
#pendingNode;
|
|
19201
21562
|
#pendingController;
|
|
19202
21563
|
#modelUnsubscribe;
|
|
19203
21564
|
#anchor;
|
|
19204
21565
|
#ownedRegistry;
|
|
19205
21566
|
#config;
|
|
21567
|
+
#renderedKeys;
|
|
21568
|
+
#lastNodeByKey;
|
|
21569
|
+
#enteringKeys;
|
|
21570
|
+
#leaving;
|
|
21571
|
+
#leavingAnimating;
|
|
21572
|
+
#treeAnimReady;
|
|
21573
|
+
willUpdate(changedProps) {
|
|
21574
|
+
super.willUpdate(changedProps);
|
|
21575
|
+
this.#diffVisibleNodes();
|
|
21576
|
+
}
|
|
21577
|
+
#diffVisibleNodes() {
|
|
21578
|
+
const nodes = this.root ? this.#getAllNodes() : [];
|
|
21579
|
+
const nowKeys = new Set(nodes.map((n) => n.node.item.key));
|
|
21580
|
+
this.#enteringKeys.clear();
|
|
21581
|
+
if (this.#treeAnimReady && this.#renderedKeys.length > 0) {
|
|
21582
|
+
const prevSet = new Set(this.#renderedKeys);
|
|
21583
|
+
for (const { node } of nodes) if (!prevSet.has(node.item.key)) this.#enteringKeys.add(node.item.key);
|
|
21584
|
+
if (this.#enteringKeys.size > MAX_ANIMATED_ROWS) this.#enteringKeys.clear();
|
|
21585
|
+
for (const key of this.#renderedKeys) if (!nowKeys.has(key) && !this.#leaving.has(key)) {
|
|
21586
|
+
const last = this.#lastNodeByKey.get(key);
|
|
21587
|
+
if (last) this.#leaving.set(key, last);
|
|
21588
|
+
}
|
|
21589
|
+
}
|
|
21590
|
+
for (const key of [...this.#leaving.keys()]) if (nowKeys.has(key)) {
|
|
21591
|
+
this.#leaving.delete(key);
|
|
21592
|
+
this.#leavingAnimating.delete(key);
|
|
21593
|
+
}
|
|
21594
|
+
if (this.#leaving.size > MAX_ANIMATED_ROWS) {
|
|
21595
|
+
this.#leaving.clear();
|
|
21596
|
+
this.#leavingAnimating.clear();
|
|
21597
|
+
}
|
|
21598
|
+
}
|
|
21599
|
+
/** True while `node` is a fading ghost row (removed / collapsed, still animating out). */
|
|
21600
|
+
_isLeaving(node) {
|
|
21601
|
+
return this.#leaving.has(node.item.key);
|
|
21602
|
+
}
|
|
21603
|
+
/**
|
|
21604
|
+
* True when `filterPredicate` is set and neither `node` nor any descendant
|
|
21605
|
+
* matches — the row should carry the `filtered` class (`display: none`).
|
|
21606
|
+
*/
|
|
21607
|
+
_isFilteredOut(node, depth) {
|
|
21608
|
+
return this.filterPredicate !== void 0 && !this.#matchesFilter(node, depth);
|
|
21609
|
+
}
|
|
21610
|
+
/** Splice the fading ghost rows back into the visible list near where they were. */
|
|
21611
|
+
#weaveLeaving(nodes) {
|
|
21612
|
+
if (this.#leaving.size === 0) return nodes;
|
|
21613
|
+
const result = [...nodes];
|
|
21614
|
+
const indexOfKey = (key) => result.findIndex((e) => e.node.item.key === key);
|
|
21615
|
+
for (const [key, entry] of this.#leaving) {
|
|
21616
|
+
const prevIdx = this.#renderedKeys.indexOf(key);
|
|
21617
|
+
let insertAt = result.length;
|
|
21618
|
+
for (let j = prevIdx + 1; j < this.#renderedKeys.length; j++) {
|
|
21619
|
+
const at = indexOfKey(this.#renderedKeys[j]);
|
|
21620
|
+
if (at !== -1) {
|
|
21621
|
+
insertAt = at;
|
|
21622
|
+
break;
|
|
21623
|
+
}
|
|
21624
|
+
}
|
|
21625
|
+
result.splice(insertAt, 0, entry);
|
|
21626
|
+
}
|
|
21627
|
+
return result;
|
|
21628
|
+
}
|
|
21629
|
+
#runItemAnimations() {
|
|
21630
|
+
if (this.#enteringKeys.size > 0) {
|
|
21631
|
+
for (const el of this.renderRoot.querySelectorAll(`.item`)) {
|
|
21632
|
+
const key = el.dataset.key;
|
|
21633
|
+
if (key !== void 0 && this.#enteringKeys.has(key)) animateListItemIn(el);
|
|
21634
|
+
}
|
|
21635
|
+
this.#enteringKeys.clear();
|
|
21636
|
+
}
|
|
21637
|
+
if (this.#leaving.size > 0) for (const el of this.renderRoot.querySelectorAll(`.item.leaving`)) {
|
|
21638
|
+
const key = el.dataset.key;
|
|
21639
|
+
if (key === void 0 || this.#leavingAnimating.has(key)) continue;
|
|
21640
|
+
this.#leavingAnimating.add(key);
|
|
21641
|
+
animateListItemOut(el).then(() => {
|
|
21642
|
+
this.#leaving.delete(key);
|
|
21643
|
+
this.#leavingAnimating.delete(key);
|
|
21644
|
+
this.requestUpdate();
|
|
21645
|
+
});
|
|
21646
|
+
}
|
|
21647
|
+
const visible = this.root ? this.#getAllNodes() : [];
|
|
21648
|
+
this.#renderedKeys = visible.map((n) => n.node.item.key);
|
|
21649
|
+
this.#lastNodeByKey.clear();
|
|
21650
|
+
for (const entry of visible) this.#lastNodeByKey.set(entry.node.item.key, entry);
|
|
21651
|
+
this.#treeAnimReady = true;
|
|
21652
|
+
}
|
|
19206
21653
|
updated(changedProps) {
|
|
19207
21654
|
if (changedProps.has(`root`) && this.root) {
|
|
19208
21655
|
this.#abortPending();
|
|
@@ -19237,6 +21684,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19237
21684
|
if (changedProps.has(`selectedNode`) && this.selectedNode) {
|
|
19238
21685
|
if (changedProps.get(`selectedNode`) !== this.selectedNode) this.tickled.setTickled(this.selectedNode.item.key);
|
|
19239
21686
|
}
|
|
21687
|
+
this.#runItemAnimations();
|
|
19240
21688
|
}
|
|
19241
21689
|
connectedCallback() {
|
|
19242
21690
|
super.connectedCallback();
|
|
@@ -19255,11 +21703,14 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19255
21703
|
execute: () => this.deselectAll()
|
|
19256
21704
|
});
|
|
19257
21705
|
this.registry = registry;
|
|
21706
|
+
this.#syncSearchCommands();
|
|
19258
21707
|
}
|
|
19259
21708
|
disconnectedCallback() {
|
|
19260
21709
|
super.disconnectedCallback();
|
|
19261
21710
|
this.#modelUnsubscribe?.();
|
|
19262
21711
|
this.#modelUnsubscribe = void 0;
|
|
21712
|
+
this.#unregisterSearchCommands?.();
|
|
21713
|
+
this.#unregisterSearchCommands = void 0;
|
|
19263
21714
|
if (this.#ownedRegistry) {
|
|
19264
21715
|
this.#ownedRegistry.unregister(`select-all`);
|
|
19265
21716
|
this.#ownedRegistry.unregister(`deselect-all`);
|
|
@@ -19357,6 +21808,28 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19357
21808
|
if (!keyPath) return false;
|
|
19358
21809
|
return this.navigateTo(keyPath);
|
|
19359
21810
|
}
|
|
21811
|
+
/** Expand ancestors + move the cursor to `key`, without touching the selection (search "navigate" mode). */
|
|
21812
|
+
revealKey(key) {
|
|
21813
|
+
if (!this.root?.children) return false;
|
|
21814
|
+
const path = this.#findNodePath(this.root.children, (n) => n.item.key === key);
|
|
21815
|
+
if (!path) return false;
|
|
21816
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
21817
|
+
this.#applyExclusivity(path[i]);
|
|
21818
|
+
this._expandedKeys.add(path[i]);
|
|
21819
|
+
}
|
|
21820
|
+
this.tickled.setTickled(key);
|
|
21821
|
+
this.requestUpdate();
|
|
21822
|
+
this.updateComplete.then(() => this.#scrollKeyIntoView(key));
|
|
21823
|
+
return true;
|
|
21824
|
+
}
|
|
21825
|
+
/** Replace the selection with `nodes` (subject to `selectionFilter`) — search "select" mode. */
|
|
21826
|
+
selectMany(nodes) {
|
|
21827
|
+
const prev = new Set(this._selectedNodes);
|
|
21828
|
+
this._selectedNodes = new Set([...nodes].filter((n) => this.#canSelect(n)));
|
|
21829
|
+
this.selectedNode = [...this._selectedNodes].at(-1);
|
|
21830
|
+
this.#dispatchSelect(prev);
|
|
21831
|
+
this.requestUpdate();
|
|
21832
|
+
}
|
|
19360
21833
|
getNodes() {
|
|
19361
21834
|
if (!this.root?.children) return [];
|
|
19362
21835
|
const result = [];
|
|
@@ -19401,9 +21874,9 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19401
21874
|
}
|
|
19402
21875
|
}
|
|
19403
21876
|
}
|
|
19404
|
-
renderNodeContent(node,
|
|
21877
|
+
renderNodeContent(node, depth) {
|
|
19405
21878
|
const isBranch = node.item.isLeaf !== true;
|
|
19406
|
-
const isExpanded = this._isExpanded(node);
|
|
21879
|
+
const isExpanded = this._isExpanded(node, depth);
|
|
19407
21880
|
return b`
|
|
19408
21881
|
<span class="tree-caret">
|
|
19409
21882
|
${isBranch ? b`<ixfx-icon class="caret-icon" name="chevron-down" .rotate=${isExpanded ? A : `-90deg`}></ixfx-icon>` : ``}
|
|
@@ -19684,9 +22157,9 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19684
22157
|
_isFocused(node) {
|
|
19685
22158
|
return this.tickled.isTickled(node.item.key);
|
|
19686
22159
|
}
|
|
19687
|
-
_isExpanded(node) {
|
|
22160
|
+
_isExpanded(node, depth = 0) {
|
|
19688
22161
|
if (this._expandedKeys.has(node.item.key)) return true;
|
|
19689
|
-
if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child,
|
|
22162
|
+
if (this.filterPredicate && node.children) return node.children.some((child) => this.#matchesFilter(child, depth + 1));
|
|
19690
22163
|
return false;
|
|
19691
22164
|
}
|
|
19692
22165
|
#matchesFilter(node, depth) {
|
|
@@ -19760,14 +22233,14 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19760
22233
|
node: n,
|
|
19761
22234
|
depth: d
|
|
19762
22235
|
});
|
|
19763
|
-
if (
|
|
22236
|
+
if (n.children && this._isExpanded(n, d)) for (const child of n.children) visit(child, d + 1);
|
|
19764
22237
|
};
|
|
19765
22238
|
if (node.children) for (const child of node.children) visit(child, depth + 1);
|
|
19766
22239
|
return result;
|
|
19767
22240
|
}
|
|
19768
22241
|
#getAllNodes() {
|
|
19769
22242
|
if (!this.root) return [];
|
|
19770
|
-
return this.#getVisibleNodes(this.root,
|
|
22243
|
+
return this.#getVisibleNodes(this.root, -1);
|
|
19771
22244
|
}
|
|
19772
22245
|
#getFocusedIndex() {
|
|
19773
22246
|
const nodes = this.#getAllNodes();
|
|
@@ -19817,18 +22290,19 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19817
22290
|
>
|
|
19818
22291
|
${this.root ? this.#renderNodes(nodes) : A}
|
|
19819
22292
|
</div>
|
|
22293
|
+
${this.search.renderField()}
|
|
19820
22294
|
`;
|
|
19821
22295
|
}
|
|
19822
22296
|
#onDblClick;
|
|
19823
22297
|
#renderNodes(nodes) {
|
|
19824
|
-
if (nodes.length === 0 && this.#pendingNode) return b`
|
|
22298
|
+
if (nodes.length === 0 && this.#pendingNode && this.#leaving.size === 0) return b`
|
|
19825
22299
|
<div class="tree-loading">
|
|
19826
22300
|
${this.renderLoadingIndicator()}
|
|
19827
22301
|
</div>
|
|
19828
22302
|
`;
|
|
19829
22303
|
return b`
|
|
19830
22304
|
<ul class="tree-list" role="tree">
|
|
19831
|
-
${nodes.
|
|
22305
|
+
${repeat(this.#weaveLeaving(nodes), ({ node }) => node.item.key, ({ node, depth }) => this._renderNode(node, depth))}
|
|
19832
22306
|
</ul>
|
|
19833
22307
|
`;
|
|
19834
22308
|
}
|
|
@@ -19836,15 +22310,17 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19836
22310
|
const selected = this._isSelected(node);
|
|
19837
22311
|
const tickled = this._isFocused(node);
|
|
19838
22312
|
const isBranch = node.item.isLeaf !== true;
|
|
19839
|
-
const isExpanded = this._isExpanded(node);
|
|
22313
|
+
const isExpanded = this._isExpanded(node, depth);
|
|
19840
22314
|
const isPending = this.#pendingNode === node;
|
|
19841
|
-
const isFiltered = this.
|
|
22315
|
+
const isFiltered = this._isFilteredOut(node, depth);
|
|
22316
|
+
const isLeaving = this._isLeaving(node);
|
|
19842
22317
|
return b`
|
|
19843
22318
|
<li
|
|
19844
|
-
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``}"
|
|
22319
|
+
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${isFiltered ? `filtered` : ``} ${isLeaving ? `leaving` : ``}"
|
|
19845
22320
|
role="treeitem"
|
|
19846
22321
|
aria-selected=${selected}
|
|
19847
22322
|
aria-expanded=${isBranch ? isExpanded : A}
|
|
22323
|
+
aria-hidden=${isLeaving ? `true` : A}
|
|
19848
22324
|
aria-level=${depth + 1}
|
|
19849
22325
|
data-key=${node.item.key}
|
|
19850
22326
|
style=${`--depth: ${depth}`}
|
|
@@ -19871,6 +22347,7 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19871
22347
|
themeFallbacks,
|
|
19872
22348
|
tickledStyles,
|
|
19873
22349
|
tickledItemStyles,
|
|
22350
|
+
listSearchStyles,
|
|
19874
22351
|
i$3`
|
|
19875
22352
|
:host {
|
|
19876
22353
|
--tree-bg: var(--surface-3);
|
|
@@ -19881,16 +22358,23 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19881
22358
|
--tree-caret-size: 10px;
|
|
19882
22359
|
--tree-radius: var(--radius-s);
|
|
19883
22360
|
|
|
22361
|
+
position: relative;
|
|
19884
22362
|
display: block;
|
|
19885
|
-
overflow:
|
|
22363
|
+
overflow: hidden;
|
|
19886
22364
|
box-sizing: border-box;
|
|
19887
22365
|
background: var(--tree-bg);
|
|
19888
22366
|
color: var(--tree-text);
|
|
19889
22367
|
}
|
|
19890
22368
|
|
|
22369
|
+
/* min-height (not height) so the tree still grows to its content when the
|
|
22370
|
+
host has no explicit height; when it does, this fills it and scrolls.
|
|
22371
|
+
The scroll lives here, not on :host, so the search popover (a sibling of
|
|
22372
|
+
#container, positioned against :host) stays put while the list scrolls. */
|
|
19891
22373
|
#container {
|
|
19892
22374
|
outline: none;
|
|
19893
22375
|
min-height: 100%;
|
|
22376
|
+
max-height: 100%;
|
|
22377
|
+
overflow: auto;
|
|
19894
22378
|
}
|
|
19895
22379
|
|
|
19896
22380
|
.tree-list {
|
|
@@ -19965,6 +22449,12 @@ let TreeBaseElement = class TreeBaseElement extends i {
|
|
|
19965
22449
|
display: none;
|
|
19966
22450
|
}
|
|
19967
22451
|
|
|
22452
|
+
/* Fading-out ghost row (see #runItemAnimations) — inert while it collapses. */
|
|
22453
|
+
.item.leaving {
|
|
22454
|
+
pointer-events: none;
|
|
22455
|
+
overflow: hidden;
|
|
22456
|
+
}
|
|
22457
|
+
|
|
19968
22458
|
::highlight(incr-search-highlight) {
|
|
19969
22459
|
background: var(--search-highlight-bg, var(--accent));
|
|
19970
22460
|
color: var(--search-highlight-color, var(--accent-text, #fff));
|
|
@@ -19998,6 +22488,11 @@ __decorate([property({
|
|
|
19998
22488
|
attribute: `disable-keyboard-navigation`
|
|
19999
22489
|
})], TreeBaseElement.prototype, "disableKeyboardNavigation", void 0);
|
|
20000
22490
|
__decorate([property({ attribute: false })], TreeBaseElement.prototype, "registry", void 0);
|
|
22491
|
+
__decorate([property({
|
|
22492
|
+
type: String,
|
|
22493
|
+
attribute: `search-modes`,
|
|
22494
|
+
reflect: true
|
|
22495
|
+
})], TreeBaseElement.prototype, "searchModes", void 0);
|
|
20001
22496
|
__decorate([state()], TreeBaseElement.prototype, "_expandedKeys", void 0);
|
|
20002
22497
|
__decorate([state()], TreeBaseElement.prototype, "_focusedDepth", void 0);
|
|
20003
22498
|
TreeBaseElement = __decorate([safeCustomElement(`ixfx-tree-base`)], TreeBaseElement);
|
|
@@ -20024,11 +22519,11 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
|
|
|
20024
22519
|
if (node.item.icon) this.#depthsWithIcons.add(depth);
|
|
20025
22520
|
if (node.children) for (const child of node.children) visit(child, depth + 1);
|
|
20026
22521
|
};
|
|
20027
|
-
if (this.root) visit(this.root,
|
|
22522
|
+
if (this.root) visit(this.root, -1);
|
|
20028
22523
|
}
|
|
20029
22524
|
renderNodeContent(node, depth) {
|
|
20030
22525
|
const isBranch = node.item.isLeaf !== true;
|
|
20031
|
-
const isExpanded = this._isExpanded(node);
|
|
22526
|
+
const isExpanded = this._isExpanded(node, depth);
|
|
20032
22527
|
const icon = node.item.icon;
|
|
20033
22528
|
const tooltip = node.item.tooltip;
|
|
20034
22529
|
const caretAndLabel = b`
|
|
@@ -20074,16 +22569,19 @@ let TreeListElement = class TreeListElement extends TreeBaseElement {
|
|
|
20074
22569
|
const selected = this._isSelected(node);
|
|
20075
22570
|
const tickled = this._isFocused(node);
|
|
20076
22571
|
const isBranch = node.item.isLeaf !== true;
|
|
20077
|
-
const isExpanded = this._isExpanded(node);
|
|
22572
|
+
const isExpanded = this._isExpanded(node, depth);
|
|
20078
22573
|
const isPending = this._pendingNode === node;
|
|
20079
22574
|
const hasIconAtDepth = this.#depthsWithIcons.has(depth);
|
|
22575
|
+
const isLeaving = this._isLeaving(node);
|
|
22576
|
+
const isFiltered = this._isFilteredOut(node, depth);
|
|
20080
22577
|
return b`
|
|
20081
22578
|
<li
|
|
20082
|
-
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``}"
|
|
22579
|
+
class="item ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${isBranch ? `branch` : `leaf`} ${hasIconAtDepth ? `has-icon-at-depth` : ``} ${isLeaving ? `leaving` : ``} ${isFiltered ? `filtered` : ``}"
|
|
20083
22580
|
data-key=${node.item.key}
|
|
20084
22581
|
role="treeitem"
|
|
20085
22582
|
aria-selected=${selected}
|
|
20086
22583
|
aria-expanded=${isBranch ? isExpanded : A}
|
|
22584
|
+
aria-hidden=${isLeaving ? `true` : A}
|
|
20087
22585
|
aria-level=${depth + 1}
|
|
20088
22586
|
style=${`--depth: ${depth}`}
|
|
20089
22587
|
@click=${(e) => this._handleItemClick(node, depth, e)}
|
|
@@ -20347,15 +22845,14 @@ function createTreeListAdapter(toTreeItem, options) {
|
|
|
20347
22845
|
//#region src/vertical-list/vertical-list.ts
|
|
20348
22846
|
const ITEM_SELECTOR = `li, [data-list-item]`;
|
|
20349
22847
|
let VerticalListElement = class VerticalListElement extends i {
|
|
20350
|
-
constructor(...
|
|
20351
|
-
super(...
|
|
22848
|
+
constructor(..._args) {
|
|
22849
|
+
super(..._args);
|
|
20352
22850
|
this.selectionMode = `single`;
|
|
20353
22851
|
this.interactionMode = `standard`;
|
|
20354
22852
|
this.itemWrap = `stop`;
|
|
20355
|
-
this._searchVisible = false;
|
|
20356
|
-
this._searchQuery = ``;
|
|
20357
22853
|
this.#customWrappers = /* @__PURE__ */ new Map();
|
|
20358
22854
|
this.#wrappedElements = /* @__PURE__ */ new Set();
|
|
22855
|
+
this.#animatedItems = /* @__PURE__ */ new WeakSet();
|
|
20359
22856
|
this.selection = new ListSelectionController({
|
|
20360
22857
|
getOrderedItems: () => this._getVisibleItems(),
|
|
20361
22858
|
isSelectable: (el) => this._isCheckable(el),
|
|
@@ -20363,11 +22860,13 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20363
22860
|
selectionMode: this.selectionMode,
|
|
20364
22861
|
interactionMode: this.interactionMode
|
|
20365
22862
|
}),
|
|
22863
|
+
getKey: keyForElement,
|
|
20366
22864
|
onChange: (selected, previous) => {
|
|
20367
22865
|
this._syncItemStates();
|
|
20368
22866
|
this._dispatch(`list-select`, {
|
|
20369
22867
|
selected,
|
|
20370
|
-
previous
|
|
22868
|
+
previous,
|
|
22869
|
+
selectedKeys: this.selection.selectedKeys
|
|
20371
22870
|
});
|
|
20372
22871
|
}
|
|
20373
22872
|
});
|
|
@@ -20382,6 +22881,11 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20382
22881
|
onStateChange: () => this._syncItemStates(),
|
|
20383
22882
|
itemWrap: this.itemWrap
|
|
20384
22883
|
});
|
|
22884
|
+
this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
|
|
22885
|
+
itemSelector: ITEM_SELECTOR,
|
|
22886
|
+
getLabel: (el) => el.getAttribute(`data-search-label`) ?? el.textContent?.trim() ?? ``,
|
|
22887
|
+
highlightKey: `vertical-list-search`
|
|
22888
|
+
}) });
|
|
20385
22889
|
this._onPointerOver = (e) => {
|
|
20386
22890
|
const item = this._findListItem(e.composedPath());
|
|
20387
22891
|
if (item) this.tickled.handlePointerEnter(item);
|
|
@@ -20390,11 +22894,7 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20390
22894
|
this.tickled.handlePointerLeave(e);
|
|
20391
22895
|
};
|
|
20392
22896
|
this._onKeyDown = (e) => {
|
|
20393
|
-
if (
|
|
20394
|
-
e.preventDefault();
|
|
20395
|
-
this._openSearch();
|
|
20396
|
-
return;
|
|
20397
|
-
}
|
|
22897
|
+
if (this.search.isOpen) return;
|
|
20398
22898
|
if ((e.ctrlKey || e.metaKey) && e.key === `a`) {
|
|
20399
22899
|
if (this.selection.canSelectAll()) {
|
|
20400
22900
|
e.preventDefault();
|
|
@@ -20402,7 +22902,6 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20402
22902
|
}
|
|
20403
22903
|
return;
|
|
20404
22904
|
}
|
|
20405
|
-
if (this._searchVisible) return;
|
|
20406
22905
|
switch (e.key) {
|
|
20407
22906
|
case `ArrowDown`:
|
|
20408
22907
|
e.preventDefault();
|
|
@@ -20450,36 +22949,31 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20450
22949
|
const item = this._findListItem(e.composedPath());
|
|
20451
22950
|
if (item) this._activateItem(item);
|
|
20452
22951
|
};
|
|
20453
|
-
this._onSearchInput = (e) => {
|
|
20454
|
-
const input = e.target;
|
|
20455
|
-
this._searchQuery = input.value;
|
|
20456
|
-
this.#search?.setQuery(this._searchQuery);
|
|
20457
|
-
};
|
|
20458
|
-
this._onSearchKeydown = (e) => {
|
|
20459
|
-
if (e.key === `Escape`) {
|
|
20460
|
-
e.stopPropagation();
|
|
20461
|
-
this._closeSearch();
|
|
20462
|
-
} else if (e.key === `ArrowDown`) {
|
|
20463
|
-
e.preventDefault();
|
|
20464
|
-
this.tickled.handleKeyboardNavigation(`next`);
|
|
20465
|
-
} else if (e.key === `ArrowUp`) {
|
|
20466
|
-
e.preventDefault();
|
|
20467
|
-
this.tickled.handleKeyboardNavigation(`previous`);
|
|
20468
|
-
} else if (e.key === `Enter`) {
|
|
20469
|
-
e.preventDefault();
|
|
20470
|
-
e.stopPropagation();
|
|
20471
|
-
const id = this.tickled.tickledId ?? this._getVisibleItems()[0];
|
|
20472
|
-
if (id) {
|
|
20473
|
-
this._dispatch(`list-item-click`, { item: id });
|
|
20474
|
-
this._activateItem(id);
|
|
20475
|
-
this._closeSearch();
|
|
20476
|
-
}
|
|
20477
|
-
}
|
|
20478
|
-
};
|
|
20479
22952
|
}
|
|
20480
|
-
#search;
|
|
20481
22953
|
#customWrappers;
|
|
20482
22954
|
#wrappedElements;
|
|
22955
|
+
/** Watches for items added to the light DOM so they can be animated in. */
|
|
22956
|
+
#itemObserver;
|
|
22957
|
+
/** Items that have already played (or been skipped for) their enter animation. */
|
|
22958
|
+
#animatedItems;
|
|
22959
|
+
#searchRegistry;
|
|
22960
|
+
#unregisterSearchCommands;
|
|
22961
|
+
/** Command registry carrying this list's `list.search.*` commands. */
|
|
22962
|
+
get registry() {
|
|
22963
|
+
return this.#searchRegistry;
|
|
22964
|
+
}
|
|
22965
|
+
#effectiveSearchModes() {
|
|
22966
|
+
const raw = this.searchModes?.trim();
|
|
22967
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
22968
|
+
const requested = new Set(raw.split(/\s+/));
|
|
22969
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
22970
|
+
}
|
|
22971
|
+
#syncSearchCommands() {
|
|
22972
|
+
if (!this.#searchRegistry) return;
|
|
22973
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
22974
|
+
this.#unregisterSearchCommands?.();
|
|
22975
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
22976
|
+
}
|
|
20483
22977
|
connectedCallback() {
|
|
20484
22978
|
super.connectedCallback();
|
|
20485
22979
|
if (!this.hasAttribute(`tabindex`)) this.tabIndex = 0;
|
|
@@ -20488,6 +22982,11 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20488
22982
|
this.addEventListener(`keydown`, this._onKeyDown);
|
|
20489
22983
|
this.addEventListener(`click`, this._onClick);
|
|
20490
22984
|
this.addEventListener(`dblclick`, this._onDblClick);
|
|
22985
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR)) this.#animatedItems.add(el);
|
|
22986
|
+
this.#itemObserver ??= new MutationObserver((records) => this.#onItemsMutated(records));
|
|
22987
|
+
this.#itemObserver.observe(this, { childList: true });
|
|
22988
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
22989
|
+
this.#syncSearchCommands();
|
|
20491
22990
|
}
|
|
20492
22991
|
disconnectedCallback() {
|
|
20493
22992
|
super.disconnectedCallback();
|
|
@@ -20496,20 +22995,35 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20496
22995
|
this.removeEventListener(`keydown`, this._onKeyDown);
|
|
20497
22996
|
this.removeEventListener(`click`, this._onClick);
|
|
20498
22997
|
this.removeEventListener(`dblclick`, this._onDblClick);
|
|
20499
|
-
this.#
|
|
20500
|
-
this.#
|
|
22998
|
+
this.#itemObserver?.disconnect();
|
|
22999
|
+
this.#unregisterSearchCommands?.();
|
|
23000
|
+
this.#unregisterSearchCommands = void 0;
|
|
23001
|
+
this.#searchRegistry = void 0;
|
|
20501
23002
|
}
|
|
20502
|
-
|
|
20503
|
-
|
|
20504
|
-
|
|
20505
|
-
|
|
20506
|
-
|
|
20507
|
-
|
|
20508
|
-
|
|
20509
|
-
|
|
23003
|
+
/**
|
|
23004
|
+
* Animate in items appended to the light DOM (however they got there —
|
|
23005
|
+
* {@link addItem}, direct `appendChild`, a framework binding). A bulk populate
|
|
23006
|
+
* (roughly everything at once) is left alone, as are the wrapper `<li>`s the
|
|
23007
|
+
* checked-mode plumbing shuffles around.
|
|
23008
|
+
*/
|
|
23009
|
+
#onItemsMutated(records) {
|
|
23010
|
+
if (!this.isConnected) return;
|
|
23011
|
+
const fresh = [];
|
|
23012
|
+
for (const record of records) for (const node of record.addedNodes) {
|
|
23013
|
+
if (!(node instanceof HTMLElement) || node.parentElement !== this) continue;
|
|
23014
|
+
if (!node.matches(ITEM_SELECTOR)) continue;
|
|
23015
|
+
if (this.#animatedItems.has(node) || this.#customWrappers.has(node) || this.#wrappedElements.has(node)) continue;
|
|
23016
|
+
this.#animatedItems.add(node);
|
|
23017
|
+
fresh.push(node);
|
|
23018
|
+
}
|
|
23019
|
+
if (fresh.length === 0) return;
|
|
23020
|
+
const total = this.querySelectorAll(ITEM_SELECTOR).length;
|
|
23021
|
+
if (fresh.length >= total) return;
|
|
23022
|
+
for (const el of fresh) animateListItemIn(el);
|
|
20510
23023
|
}
|
|
20511
23024
|
updated(changed) {
|
|
20512
23025
|
super.updated(changed);
|
|
23026
|
+
if (changed.has(`searchModes`)) this.#syncSearchCommands();
|
|
20513
23027
|
if (changed.has(`interactionMode`)) {
|
|
20514
23028
|
this.selection.clearAnchor();
|
|
20515
23029
|
const previousSelected = this.selection.selected;
|
|
@@ -20544,8 +23058,21 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20544
23058
|
this.appendChild(el);
|
|
20545
23059
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
20546
23060
|
}
|
|
23061
|
+
/**
|
|
23062
|
+
* Collapse + fade `item` out, then detach it. The rows below slide up to
|
|
23063
|
+
* close the gap. Resolves once the element has actually been removed.
|
|
23064
|
+
* Under `prefers-reduced-motion: reduce` it's removed immediately.
|
|
23065
|
+
*/
|
|
20547
23066
|
removeItem(item) {
|
|
20548
|
-
|
|
23067
|
+
this.selection.deselect(item);
|
|
23068
|
+
return animateListItemOut(item).then(() => {
|
|
23069
|
+
item.remove();
|
|
23070
|
+
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
23071
|
+
});
|
|
23072
|
+
}
|
|
23073
|
+
/** {@link removeItem} for several items at once. */
|
|
23074
|
+
removeItems(items) {
|
|
23075
|
+
return Promise.all([...items].map((item) => this.removeItem(item))).then(() => void 0);
|
|
20549
23076
|
}
|
|
20550
23077
|
clearItems() {
|
|
20551
23078
|
this.#customWrappers.clear();
|
|
@@ -20564,11 +23091,13 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20564
23091
|
if (this.interactionMode === `checked`) this._syncCheckboxes();
|
|
20565
23092
|
}
|
|
20566
23093
|
/**
|
|
20567
|
-
* Dispatch a command to this list
|
|
20568
|
-
*
|
|
20569
|
-
*
|
|
23094
|
+
* Dispatch a command to this list — used by `ixfx-grouped-item-lister` to
|
|
23095
|
+
* forward commands to all sub-components. Routes through the command registry
|
|
23096
|
+
* (so `list.search.*` and any other registered commands work).
|
|
20570
23097
|
*/
|
|
20571
|
-
dispatch(
|
|
23098
|
+
dispatch(commandId, args) {
|
|
23099
|
+
this.#searchRegistry?.invoke(commandId, args);
|
|
23100
|
+
}
|
|
20572
23101
|
select(item) {
|
|
20573
23102
|
this.selection.replace(item);
|
|
20574
23103
|
}
|
|
@@ -20584,6 +23113,43 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20584
23113
|
selectAll() {
|
|
20585
23114
|
if (this.selection.canSelectAll()) this.selection.selectAll();
|
|
20586
23115
|
}
|
|
23116
|
+
/** The selection as stable `data-key` / `data-value` strings. */
|
|
23117
|
+
get selectedKeys() {
|
|
23118
|
+
return this.selection.selectedKeys;
|
|
23119
|
+
}
|
|
23120
|
+
/** The key of the item under the cursor, or `undefined`. */
|
|
23121
|
+
get tickledKey() {
|
|
23122
|
+
const id = this.tickled.tickledId;
|
|
23123
|
+
return id ? keyForElement(id) : void 0;
|
|
23124
|
+
}
|
|
23125
|
+
/** Replace the selection with the items carrying these keys (unknown keys ignored). */
|
|
23126
|
+
selectKeys(keys) {
|
|
23127
|
+
this.selection.selectKeys(keys, (key) => this.#itemForKey(key));
|
|
23128
|
+
}
|
|
23129
|
+
/** Add items by key to the selection (multi-select modes only). */
|
|
23130
|
+
addSelectedKeys(keys) {
|
|
23131
|
+
this.selection.addKeys(keys, (key) => this.#itemForKey(key));
|
|
23132
|
+
}
|
|
23133
|
+
/** Remove items by key from the selection. */
|
|
23134
|
+
removeSelectedKeys(keys) {
|
|
23135
|
+
this.selection.removeKeys(keys, (key) => this.#itemForKey(key));
|
|
23136
|
+
}
|
|
23137
|
+
/** Move the cursor to the item with this key and scroll it into view. */
|
|
23138
|
+
focusKey(key) {
|
|
23139
|
+
const el = this.#itemForKey(key);
|
|
23140
|
+
if (!el) return;
|
|
23141
|
+
this.tickled.focus(el);
|
|
23142
|
+
el.scrollIntoView({
|
|
23143
|
+
block: `nearest`,
|
|
23144
|
+
inline: `nearest`
|
|
23145
|
+
});
|
|
23146
|
+
}
|
|
23147
|
+
#itemForKey(key) {
|
|
23148
|
+
for (const el of this.querySelectorAll(ITEM_SELECTOR)) {
|
|
23149
|
+
if (this.#wrappedElements.has(el)) continue;
|
|
23150
|
+
if (keyForElement(el) === key) return el;
|
|
23151
|
+
}
|
|
23152
|
+
}
|
|
20587
23153
|
*selectedFilter(kind) {
|
|
20588
23154
|
const wanted = (el) => {
|
|
20589
23155
|
switch (kind) {
|
|
@@ -20741,22 +23307,15 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20741
23307
|
composed: true
|
|
20742
23308
|
}));
|
|
20743
23309
|
}
|
|
20744
|
-
_openSearch() {
|
|
20745
|
-
this._searchVisible = true;
|
|
20746
|
-
this.updateComplete.then(() => this._searchInputEl?.focus());
|
|
20747
|
-
}
|
|
20748
|
-
_closeSearch() {
|
|
20749
|
-
this._searchVisible = false;
|
|
20750
|
-
this._searchQuery = ``;
|
|
20751
|
-
this.#search?.clearQuery();
|
|
20752
|
-
this.focus();
|
|
20753
|
-
}
|
|
20754
23310
|
_activateItem(item) {
|
|
20755
23311
|
if (this.interactionMode !== `manual`) this.selection.replace(item);
|
|
20756
23312
|
this._dispatch(`list-activate`, { item });
|
|
20757
23313
|
}
|
|
20758
23314
|
static {
|
|
20759
|
-
this.styles = [
|
|
23315
|
+
this.styles = [
|
|
23316
|
+
themeFallbacks,
|
|
23317
|
+
listSearchStyles,
|
|
23318
|
+
i$3`
|
|
20760
23319
|
:host {
|
|
20761
23320
|
display: block;
|
|
20762
23321
|
position: relative;
|
|
@@ -20807,6 +23366,10 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20807
23366
|
display: flex;
|
|
20808
23367
|
align-items: center;
|
|
20809
23368
|
padding: var(--ixfx-list-item-padding);
|
|
23369
|
+
/* Inset each row from the scroll container's edges (margin, not padding:
|
|
23370
|
+
padding is inside the border box, so the tickle/selection outlines
|
|
23371
|
+
would still sit flush against the edge and be clipped). */
|
|
23372
|
+
margin: 0 var(--list-gutter, 4px);
|
|
20810
23373
|
min-height: var(--list-item-height, 28px);
|
|
20811
23374
|
cursor: pointer;
|
|
20812
23375
|
user-select: none;
|
|
@@ -20819,6 +23382,7 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20819
23382
|
/* Custom shadow-DOM components handle their own internal layout/padding */
|
|
20820
23383
|
::slotted([data-list-item]) {
|
|
20821
23384
|
display: block;
|
|
23385
|
+
margin: 0 var(--list-gutter, 4px);
|
|
20822
23386
|
cursor: pointer;
|
|
20823
23387
|
user-select: none;
|
|
20824
23388
|
transition: background var(--transition, 0.1s ease-out),
|
|
@@ -20874,49 +23438,8 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20874
23438
|
color: inherit;
|
|
20875
23439
|
outline: var(--item-border-tickled);
|
|
20876
23440
|
}
|
|
20877
|
-
|
|
20878
|
-
|
|
20879
|
-
position: absolute;
|
|
20880
|
-
top: 8px;
|
|
20881
|
-
right: 8px;
|
|
20882
|
-
z-index: 10;
|
|
20883
|
-
display: flex;
|
|
20884
|
-
align-items: center;
|
|
20885
|
-
gap: 4px;
|
|
20886
|
-
background: var(--surface-4, #2a2a2a);
|
|
20887
|
-
border: 1px solid var(--border, #444);
|
|
20888
|
-
border-radius: var(--radius-m, 4px);
|
|
20889
|
-
padding: 4px 6px;
|
|
20890
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
|
20891
|
-
}
|
|
20892
|
-
|
|
20893
|
-
.search-overlay[hidden] {
|
|
20894
|
-
display: none;
|
|
20895
|
-
}
|
|
20896
|
-
|
|
20897
|
-
.search-input {
|
|
20898
|
-
background: transparent;
|
|
20899
|
-
border: none;
|
|
20900
|
-
outline: none;
|
|
20901
|
-
color: inherit;
|
|
20902
|
-
font-size: var(--text-m, 0.875em);
|
|
20903
|
-
min-width: 140px;
|
|
20904
|
-
}
|
|
20905
|
-
|
|
20906
|
-
.search-close {
|
|
20907
|
-
background: transparent;
|
|
20908
|
-
border: none;
|
|
20909
|
-
color: var(--surface-muted-text, #888);
|
|
20910
|
-
cursor: pointer;
|
|
20911
|
-
font-size: 1.1em;
|
|
20912
|
-
line-height: 1;
|
|
20913
|
-
padding: 0 2px;
|
|
20914
|
-
}
|
|
20915
|
-
|
|
20916
|
-
.search-close:hover {
|
|
20917
|
-
color: inherit;
|
|
20918
|
-
}
|
|
20919
|
-
`];
|
|
23441
|
+
`
|
|
23442
|
+
];
|
|
20920
23443
|
}
|
|
20921
23444
|
render() {
|
|
20922
23445
|
return b`
|
|
@@ -20924,21 +23447,7 @@ let VerticalListElement = class VerticalListElement extends i {
|
|
|
20924
23447
|
<ul part="list">
|
|
20925
23448
|
<slot></slot>
|
|
20926
23449
|
</ul>
|
|
20927
|
-
|
|
20928
|
-
class="search-overlay"
|
|
20929
|
-
part="search-overlay"
|
|
20930
|
-
?hidden=${!this._searchVisible}
|
|
20931
|
-
>
|
|
20932
|
-
<input
|
|
20933
|
-
type="text"
|
|
20934
|
-
class="search-input"
|
|
20935
|
-
placeholder="Search…"
|
|
20936
|
-
.value=${this._searchQuery}
|
|
20937
|
-
@input=${this._onSearchInput}
|
|
20938
|
-
@keydown=${this._onSearchKeydown}
|
|
20939
|
-
/>
|
|
20940
|
-
<button class="search-close" @click=${this._closeSearch} title="Close search (Esc)">×</button>
|
|
20941
|
-
</div>
|
|
23450
|
+
${this.search.renderField()}
|
|
20942
23451
|
</div>
|
|
20943
23452
|
`;
|
|
20944
23453
|
}
|
|
@@ -20957,9 +23466,11 @@ __decorate([property({
|
|
|
20957
23466
|
attribute: `item-wrap`
|
|
20958
23467
|
})], VerticalListElement.prototype, "itemWrap", void 0);
|
|
20959
23468
|
__decorate([property({ attribute: false })], VerticalListElement.prototype, "checkboxRenderer", void 0);
|
|
20960
|
-
__decorate([
|
|
20961
|
-
|
|
20962
|
-
|
|
23469
|
+
__decorate([property({
|
|
23470
|
+
type: String,
|
|
23471
|
+
attribute: `search-modes`,
|
|
23472
|
+
reflect: true
|
|
23473
|
+
})], VerticalListElement.prototype, "searchModes", void 0);
|
|
20963
23474
|
VerticalListElement = __decorate([safeCustomElement(`ixfx-vertical-list`)], VerticalListElement);
|
|
20964
23475
|
//#endregion
|
|
20965
23476
|
//#region src/grouped-item-lister/vertical-list-adapter.ts
|
|
@@ -25457,6 +27968,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25457
27968
|
constructor(..._args) {
|
|
25458
27969
|
super(..._args);
|
|
25459
27970
|
this.headers = false;
|
|
27971
|
+
this.stretchLastColumn = false;
|
|
25460
27972
|
this.interactionMode = `standard`;
|
|
25461
27973
|
this.selectionFilter = `leaf`;
|
|
25462
27974
|
this.exclusivity = `none`;
|
|
@@ -25468,6 +27980,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25468
27980
|
this.#resizingDepth = -1;
|
|
25469
27981
|
this.#resizeStartX = 0;
|
|
25470
27982
|
this.#resizeStartWidth = 0;
|
|
27983
|
+
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
27984
|
+
labelSelector: `.label`,
|
|
27985
|
+
highlightKey: `incr-search-highlight`
|
|
27986
|
+
}) });
|
|
25471
27987
|
this.tickled = new TickledController(this, { getItems: () => {
|
|
25472
27988
|
const items = [];
|
|
25473
27989
|
for (let depth = 0; depth < this._columns.length; depth++) {
|
|
@@ -25477,6 +27993,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25477
27993
|
return items;
|
|
25478
27994
|
} });
|
|
25479
27995
|
this.#onKeyDown = (event) => {
|
|
27996
|
+
if (this.search.isOpen) return;
|
|
25480
27997
|
const currentDepth = this.#getTickledDepthOrDefault();
|
|
25481
27998
|
const col = this._columns[currentDepth];
|
|
25482
27999
|
if (!col || col.length === 0) {
|
|
@@ -25683,9 +28200,31 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25683
28200
|
#resizeStartX;
|
|
25684
28201
|
#resizeStartWidth;
|
|
25685
28202
|
#modelUnsubscribe;
|
|
28203
|
+
#searchRegistry;
|
|
28204
|
+
#unregisterSearchCommands;
|
|
28205
|
+
/** Command registry carrying this component's `list.search.*` commands. */
|
|
28206
|
+
get registry() {
|
|
28207
|
+
return this.#searchRegistry;
|
|
28208
|
+
}
|
|
28209
|
+
#effectiveSearchModes() {
|
|
28210
|
+
const raw = this.searchModes?.trim();
|
|
28211
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
28212
|
+
const requested = new Set(raw.split(/\s+/));
|
|
28213
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
28214
|
+
}
|
|
28215
|
+
#syncSearchCommands() {
|
|
28216
|
+
if (!this.#searchRegistry) return;
|
|
28217
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
28218
|
+
this.#unregisterSearchCommands?.();
|
|
28219
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#searchRegistry, this.search);
|
|
28220
|
+
}
|
|
25686
28221
|
#buildTickledId(node, depth) {
|
|
25687
28222
|
return `${depth}:${node.item.key}`;
|
|
25688
28223
|
}
|
|
28224
|
+
/** The node column 0 renders the children of: `viewRoot` when rebased, else `root`. */
|
|
28225
|
+
get #displayRoot() {
|
|
28226
|
+
return this.viewRoot ?? this.root;
|
|
28227
|
+
}
|
|
25689
28228
|
#getTickledDepth() {
|
|
25690
28229
|
const id = this.tickled.tickledId;
|
|
25691
28230
|
if (!id) return void 0;
|
|
@@ -25694,16 +28233,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25694
28233
|
return Number(id.substring(0, colonIndex));
|
|
25695
28234
|
}
|
|
25696
28235
|
updated(changedProps) {
|
|
28236
|
+
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
25697
28237
|
if (changedProps.has(`root`) && this.root) {
|
|
25698
28238
|
this.#abortPending();
|
|
25699
|
-
this.
|
|
28239
|
+
if (this.viewRoot !== void 0) this.viewRoot = void 0;
|
|
25700
28240
|
this._selectedByDepth.clear();
|
|
25701
28241
|
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
25702
28242
|
this._selectedNodes.clear();
|
|
25703
28243
|
this.#lastSelectedNode = void 0;
|
|
25704
28244
|
this.#anchor = void 0;
|
|
25705
28245
|
this.tickled.clearTickled();
|
|
28246
|
+
const display = this.#displayRoot;
|
|
28247
|
+
this._columns = display?.children ? [display.children] : [];
|
|
25706
28248
|
}
|
|
28249
|
+
if (changedProps.has(`viewRoot`)) this.#applyViewRoot();
|
|
25707
28250
|
if (changedProps.has(`model`)) {
|
|
25708
28251
|
this.#modelUnsubscribe?.();
|
|
25709
28252
|
this.#modelUnsubscribe = void 0;
|
|
@@ -25745,10 +28288,18 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25745
28288
|
});
|
|
25746
28289
|
}
|
|
25747
28290
|
}
|
|
28291
|
+
connectedCallback() {
|
|
28292
|
+
super.connectedCallback();
|
|
28293
|
+
this.#searchRegistry ??= createCommandRegistry();
|
|
28294
|
+
this.#syncSearchCommands();
|
|
28295
|
+
}
|
|
25748
28296
|
disconnectedCallback() {
|
|
25749
28297
|
super.disconnectedCallback();
|
|
25750
28298
|
this.#modelUnsubscribe?.();
|
|
25751
28299
|
this.#modelUnsubscribe = void 0;
|
|
28300
|
+
this.#unregisterSearchCommands?.();
|
|
28301
|
+
this.#unregisterSearchCommands = void 0;
|
|
28302
|
+
this.#searchRegistry = void 0;
|
|
25752
28303
|
}
|
|
25753
28304
|
getConfig() {
|
|
25754
28305
|
const columnWidths = {};
|
|
@@ -25844,15 +28395,17 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25844
28395
|
return results;
|
|
25845
28396
|
}
|
|
25846
28397
|
/**
|
|
25847
|
-
* Programmatically navigate to a node by key path
|
|
28398
|
+
* Programmatically navigate to a node by key path, relative to the current
|
|
28399
|
+
* view root (rebased or full tree).
|
|
25848
28400
|
*/
|
|
25849
28401
|
navigateTo(keys) {
|
|
25850
|
-
|
|
28402
|
+
const display = this.#displayRoot;
|
|
28403
|
+
if (!display?.children) return false;
|
|
25851
28404
|
this.#abortPending();
|
|
25852
28405
|
this._selectedByDepth.clear();
|
|
25853
28406
|
this.tickled.clearTickled();
|
|
25854
|
-
const newColumns = [
|
|
25855
|
-
let currentColumn =
|
|
28407
|
+
const newColumns = [display.children];
|
|
28408
|
+
let currentColumn = display.children;
|
|
25856
28409
|
let success = true;
|
|
25857
28410
|
let lastNode;
|
|
25858
28411
|
let lastDepth = 0;
|
|
@@ -25887,13 +28440,51 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25887
28440
|
* Programmatically navigate to the first node (breadth-first) matching a predicate.
|
|
25888
28441
|
*/
|
|
25889
28442
|
navigateToNode(predicate) {
|
|
25890
|
-
|
|
25891
|
-
|
|
28443
|
+
const display = this.#displayRoot;
|
|
28444
|
+
if (!display?.children) return false;
|
|
28445
|
+
const keyPath = this.#findNodePath(display.children, predicate);
|
|
25892
28446
|
if (!keyPath) return false;
|
|
25893
28447
|
return this.navigateTo(keyPath);
|
|
25894
28448
|
}
|
|
28449
|
+
/** Open the columns down to `key` and move the cursor to it, without changing the selection (search "navigate" mode). */
|
|
28450
|
+
revealKey(key) {
|
|
28451
|
+
const display = this.#displayRoot;
|
|
28452
|
+
if (!display?.children) return false;
|
|
28453
|
+
const path = this.#findNodePath(display.children, (n) => n.item.key === key);
|
|
28454
|
+
if (!path) return false;
|
|
28455
|
+
const columns = [display.children];
|
|
28456
|
+
let current = display.children;
|
|
28457
|
+
let lastNode;
|
|
28458
|
+
let lastDepth = 0;
|
|
28459
|
+
for (let depth = 0; depth < path.length; depth++) {
|
|
28460
|
+
const node = current.find((n) => n.item.key === path[depth]);
|
|
28461
|
+
if (!node) return false;
|
|
28462
|
+
lastNode = node;
|
|
28463
|
+
lastDepth = depth;
|
|
28464
|
+
if (node.children && node.children.length > 0) {
|
|
28465
|
+
columns.push(node.children);
|
|
28466
|
+
current = node.children;
|
|
28467
|
+
} else break;
|
|
28468
|
+
}
|
|
28469
|
+
this._columns = columns;
|
|
28470
|
+
if (lastNode) this.tickled.setTickled(this.#buildTickledId(lastNode, lastDepth));
|
|
28471
|
+
this.requestUpdate();
|
|
28472
|
+
this.#scrollNavigatedItemIntoView();
|
|
28473
|
+
return true;
|
|
28474
|
+
}
|
|
28475
|
+
/** Replace the selection with `nodes` (search "select" mode). */
|
|
28476
|
+
selectMany(nodes) {
|
|
28477
|
+
const arr = this.selectionFilter === `none` ? [] : [...nodes];
|
|
28478
|
+
const prev = new Set(this._selectedNodes);
|
|
28479
|
+
this._selectedNodes = new Set(arr);
|
|
28480
|
+
this._selectedKeys = new Set(arr.map((n) => n.item.key));
|
|
28481
|
+
this.#lastSelectedNode = arr.at(-1);
|
|
28482
|
+
this.#dispatchSelectEvent(prev);
|
|
28483
|
+
this.requestUpdate();
|
|
28484
|
+
}
|
|
25895
28485
|
getNodes() {
|
|
25896
|
-
|
|
28486
|
+
const display = this.#displayRoot;
|
|
28487
|
+
if (!display?.children) return [];
|
|
25897
28488
|
const result = [];
|
|
25898
28489
|
const visit = (node, depth) => {
|
|
25899
28490
|
result.push({
|
|
@@ -25902,7 +28493,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25902
28493
|
});
|
|
25903
28494
|
if (node.children) for (const child of node.children) visit(child, depth + 1);
|
|
25904
28495
|
};
|
|
25905
|
-
for (const child of
|
|
28496
|
+
for (const child of display.children) visit(child, 0);
|
|
25906
28497
|
return result;
|
|
25907
28498
|
}
|
|
25908
28499
|
#findNodePath(nodes, predicate, prefix = []) {
|
|
@@ -25915,6 +28506,124 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
25915
28506
|
}
|
|
25916
28507
|
}
|
|
25917
28508
|
}
|
|
28509
|
+
/**
|
|
28510
|
+
* Re-render the columns for the current `#displayRoot`, clearing selection.
|
|
28511
|
+
* Runs on every `viewRoot` change.
|
|
28512
|
+
*/
|
|
28513
|
+
#applyViewRoot() {
|
|
28514
|
+
this.#abortPending();
|
|
28515
|
+
const prev = new Set(this._selectedNodes);
|
|
28516
|
+
this._selectedByDepth.clear();
|
|
28517
|
+
this._selectedKeys = /* @__PURE__ */ new Set();
|
|
28518
|
+
this._selectedNodes.clear();
|
|
28519
|
+
this.#lastSelectedNode = void 0;
|
|
28520
|
+
this.#anchor = void 0;
|
|
28521
|
+
this.tickled.clearTickled();
|
|
28522
|
+
const display = this.#displayRoot;
|
|
28523
|
+
if (display?.children !== void 0) {
|
|
28524
|
+
this._columns = display.children ? [display.children] : [];
|
|
28525
|
+
this._loadingDepth = void 0;
|
|
28526
|
+
} else if (display) {
|
|
28527
|
+
this._columns = [];
|
|
28528
|
+
this.#loadUnloadedChildren(display, -1, false);
|
|
28529
|
+
} else {
|
|
28530
|
+
this._columns = [];
|
|
28531
|
+
this._loadingDepth = void 0;
|
|
28532
|
+
}
|
|
28533
|
+
if (prev.size > 0) this.#dispatchSelectEvent(prev);
|
|
28534
|
+
}
|
|
28535
|
+
/**
|
|
28536
|
+
* Rebase the view to show `target`'s children in column 0 (Finder column-view
|
|
28537
|
+
* behavior on double-click), keeping the full tree in `root`.
|
|
28538
|
+
*
|
|
28539
|
+
* - Pass a `MillerNode` (used as-is) or a key (looked up in the full tree).
|
|
28540
|
+
* - Pass `undefined` to restore the full-tree view.
|
|
28541
|
+
* - Rebasing to the true root is equivalent to restoring the full view.
|
|
28542
|
+
* - Leaves are refused (returns `false`).
|
|
28543
|
+
*
|
|
28544
|
+
* Selection is cleared; all depths, headers, and key paths are relative to
|
|
28545
|
+
* the new view root. Fires a `rebase` event when the view actually changes.
|
|
28546
|
+
* Returns `false` when nothing changed (unknown key, leaf, or same view).
|
|
28547
|
+
*/
|
|
28548
|
+
rebaseTo(target) {
|
|
28549
|
+
if (target === void 0) return this.#clearViewRoot();
|
|
28550
|
+
const node = typeof target === `string` ? this.#findNodeByKey(target) : target;
|
|
28551
|
+
if (!node || node.item.isLeaf === true) return false;
|
|
28552
|
+
if (node === this.viewRoot || node.item.key === this.viewRoot?.item.key) return false;
|
|
28553
|
+
if (node === this.root || !this.viewRoot && node.item.key === this.root?.item.key) return this.#clearViewRoot();
|
|
28554
|
+
this.viewRoot = node;
|
|
28555
|
+
this.#dispatchRebaseEvent();
|
|
28556
|
+
return true;
|
|
28557
|
+
}
|
|
28558
|
+
/**
|
|
28559
|
+
* Rebase one level up from the current view root (no-op at the full tree).
|
|
28560
|
+
* Fires a `rebase` event when the view actually changes.
|
|
28561
|
+
*/
|
|
28562
|
+
rebaseUp() {
|
|
28563
|
+
if (!this.viewRoot) return false;
|
|
28564
|
+
const parent = this.#findParentOf(this.viewRoot);
|
|
28565
|
+
if (!parent) return false;
|
|
28566
|
+
if (parent === this.root) return this.#clearViewRoot();
|
|
28567
|
+
this.viewRoot = parent;
|
|
28568
|
+
this.#dispatchRebaseEvent();
|
|
28569
|
+
return true;
|
|
28570
|
+
}
|
|
28571
|
+
#clearViewRoot() {
|
|
28572
|
+
if (this.viewRoot === void 0) return false;
|
|
28573
|
+
this.viewRoot = void 0;
|
|
28574
|
+
this.#dispatchRebaseEvent();
|
|
28575
|
+
return true;
|
|
28576
|
+
}
|
|
28577
|
+
#dispatchRebaseEvent() {
|
|
28578
|
+
const viewRoot = this.viewRoot;
|
|
28579
|
+
const detail = {
|
|
28580
|
+
viewRoot,
|
|
28581
|
+
path: viewRoot ? this.#pathToRoot(viewRoot) : []
|
|
28582
|
+
};
|
|
28583
|
+
this.dispatchEvent(new CustomEvent(`rebase`, {
|
|
28584
|
+
detail,
|
|
28585
|
+
bubbles: true,
|
|
28586
|
+
composed: true
|
|
28587
|
+
}));
|
|
28588
|
+
}
|
|
28589
|
+
#findNodeByKey(key) {
|
|
28590
|
+
if (!this.root) return void 0;
|
|
28591
|
+
if (this.root.item.key === key) return this.root;
|
|
28592
|
+
return this.#findNodeIn(this.root.children ?? [], key);
|
|
28593
|
+
}
|
|
28594
|
+
#findNodeIn(nodes, key) {
|
|
28595
|
+
for (const node of nodes) {
|
|
28596
|
+
if (node.item.key === key) return node;
|
|
28597
|
+
if (node.children) {
|
|
28598
|
+
const found = this.#findNodeIn(node.children, key);
|
|
28599
|
+
if (found) return found;
|
|
28600
|
+
}
|
|
28601
|
+
}
|
|
28602
|
+
}
|
|
28603
|
+
/** Find the parent of `target` (by reference, falling back to key) in the full tree. */
|
|
28604
|
+
#findParentOf(target) {
|
|
28605
|
+
if (!this.root || target === this.root) return void 0;
|
|
28606
|
+
return this.#findParentIn(this.root, target);
|
|
28607
|
+
}
|
|
28608
|
+
#findParentIn(node, target) {
|
|
28609
|
+
if (!node.children) return void 0;
|
|
28610
|
+
for (const child of node.children) {
|
|
28611
|
+
if (child === target || child.item.key === target.item.key) return node;
|
|
28612
|
+
const found = this.#findParentIn(child, target);
|
|
28613
|
+
if (found) return found;
|
|
28614
|
+
}
|
|
28615
|
+
}
|
|
28616
|
+
/** Ancestry chain from the true root down to and including `target`. */
|
|
28617
|
+
#pathToRoot(target) {
|
|
28618
|
+
const chain = [target];
|
|
28619
|
+
let current = this.#findParentOf(target);
|
|
28620
|
+
while (current) {
|
|
28621
|
+
chain.unshift(current);
|
|
28622
|
+
if (current === this.root) break;
|
|
28623
|
+
current = this.#findParentOf(current);
|
|
28624
|
+
}
|
|
28625
|
+
return chain;
|
|
28626
|
+
}
|
|
25918
28627
|
renderColumnContent(nodes, depth) {
|
|
25919
28628
|
console.warn(`ixfx-miller-base: renderColumnContent not overridden. depth=${depth}, nodes=${nodes.length}`);
|
|
25920
28629
|
return A;
|
|
@@ -26041,7 +28750,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26041
28750
|
*/
|
|
26042
28751
|
#buildPath(depth) {
|
|
26043
28752
|
const path = [];
|
|
26044
|
-
if (this
|
|
28753
|
+
if (this.#displayRoot) path.push(this.#displayRoot);
|
|
26045
28754
|
for (let d = 0; d < depth; d++) {
|
|
26046
28755
|
const sel = this._selectedByDepth.get(d);
|
|
26047
28756
|
if (sel) path.push(sel);
|
|
@@ -26144,57 +28853,8 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26144
28853
|
this.requestUpdate();
|
|
26145
28854
|
return;
|
|
26146
28855
|
}
|
|
26147
|
-
if (node.children === void 0)
|
|
26148
|
-
|
|
26149
|
-
this._loadingDepth = depth + 1;
|
|
26150
|
-
this.#pendingNode = node;
|
|
26151
|
-
const controller = new AbortController();
|
|
26152
|
-
this.#pendingController = controller;
|
|
26153
|
-
this.dispatchEvent(new CustomEvent(`expand`, {
|
|
26154
|
-
detail: {
|
|
26155
|
-
node,
|
|
26156
|
-
depth
|
|
26157
|
-
},
|
|
26158
|
-
bubbles: true,
|
|
26159
|
-
composed: true
|
|
26160
|
-
}));
|
|
26161
|
-
const applyChildren = (children) => {
|
|
26162
|
-
if (controller.signal.aborted) return;
|
|
26163
|
-
node.children = children;
|
|
26164
|
-
this.#pendingNode = void 0;
|
|
26165
|
-
this.#pendingController = void 0;
|
|
26166
|
-
this._loadingDepth = void 0;
|
|
26167
|
-
const cols = this._columns.slice(0, depth + 1);
|
|
26168
|
-
if (children.length > 0) cols.push(children);
|
|
26169
|
-
this._columns = cols;
|
|
26170
|
-
};
|
|
26171
|
-
const handleError = (err) => {
|
|
26172
|
-
if (err.name !== `AbortError`) console.error(err);
|
|
26173
|
-
node.children = [];
|
|
26174
|
-
this.#pendingNode = void 0;
|
|
26175
|
-
this.#pendingController = void 0;
|
|
26176
|
-
this._loadingDepth = void 0;
|
|
26177
|
-
this.requestUpdate();
|
|
26178
|
-
};
|
|
26179
|
-
if (this.model || this.loadChildren) {
|
|
26180
|
-
if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
|
|
26181
|
-
this.#expandTimer = setTimeout(() => {
|
|
26182
|
-
this.#expandTimer = void 0;
|
|
26183
|
-
if (this.#pendingNode !== node) return;
|
|
26184
|
-
(this.model ? this.model.getChildren(node, depth, controller.signal) : collectDataResult(this.loadChildren({
|
|
26185
|
-
node,
|
|
26186
|
-
depth
|
|
26187
|
-
}, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
|
|
26188
|
-
}, EXPAND_DEBOUNCE_MS);
|
|
26189
|
-
} else {
|
|
26190
|
-
this._loadingDepth = void 0;
|
|
26191
|
-
this.#pendingNode = void 0;
|
|
26192
|
-
this.#pendingController = void 0;
|
|
26193
|
-
const newColumns = this._columns.slice(0, depth + 1);
|
|
26194
|
-
newColumns.push([]);
|
|
26195
|
-
this._columns = newColumns;
|
|
26196
|
-
}
|
|
26197
|
-
} else {
|
|
28856
|
+
if (node.children === void 0) this.#loadUnloadedChildren(node, depth, true);
|
|
28857
|
+
else {
|
|
26198
28858
|
const newColumns = this._columns.slice(0, depth + 1);
|
|
26199
28859
|
newColumns.push(node.children);
|
|
26200
28860
|
this._columns = newColumns;
|
|
@@ -26208,6 +28868,68 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26208
28868
|
}));
|
|
26209
28869
|
}
|
|
26210
28870
|
}
|
|
28871
|
+
/**
|
|
28872
|
+
* Fetch children for a node whose `children === undefined` and show them as
|
|
28873
|
+
* the column after `parentDepth` (with the loading placeholder meanwhile).
|
|
28874
|
+
* Shared by branch expansion (`parentDepth` = the branch's column) and view
|
|
28875
|
+
* rebasing (`parentDepth` = -1, so the children become column 0).
|
|
28876
|
+
*
|
|
28877
|
+
* The `depth` reported to `loadChildren`/`model.getChildren` is the node's
|
|
28878
|
+
* own depth: its column for expansions, -1 for a view root (it sits above
|
|
28879
|
+
* column 0, so loaders computing child depth as `depth + 1` stay correct).
|
|
28880
|
+
*/
|
|
28881
|
+
#loadUnloadedChildren(node, parentDepth, fireExpandEvent) {
|
|
28882
|
+
const nodeDepth = parentDepth + 1;
|
|
28883
|
+
this._columns = this._columns.slice(0, nodeDepth);
|
|
28884
|
+
this._loadingDepth = nodeDepth;
|
|
28885
|
+
this.#pendingNode = node;
|
|
28886
|
+
const controller = new AbortController();
|
|
28887
|
+
this.#pendingController = controller;
|
|
28888
|
+
if (fireExpandEvent) this.dispatchEvent(new CustomEvent(`expand`, {
|
|
28889
|
+
detail: {
|
|
28890
|
+
node,
|
|
28891
|
+
depth: parentDepth
|
|
28892
|
+
},
|
|
28893
|
+
bubbles: true,
|
|
28894
|
+
composed: true
|
|
28895
|
+
}));
|
|
28896
|
+
const applyChildren = (children) => {
|
|
28897
|
+
if (controller.signal.aborted) return;
|
|
28898
|
+
node.children = children;
|
|
28899
|
+
this.#pendingNode = void 0;
|
|
28900
|
+
this.#pendingController = void 0;
|
|
28901
|
+
this._loadingDepth = void 0;
|
|
28902
|
+
const cols = this._columns.slice(0, nodeDepth);
|
|
28903
|
+
if (children.length > 0) cols.push(children);
|
|
28904
|
+
this._columns = cols;
|
|
28905
|
+
};
|
|
28906
|
+
const handleError = (err) => {
|
|
28907
|
+
if (err.name !== `AbortError`) console.error(err);
|
|
28908
|
+
node.children = [];
|
|
28909
|
+
this.#pendingNode = void 0;
|
|
28910
|
+
this.#pendingController = void 0;
|
|
28911
|
+
this._loadingDepth = void 0;
|
|
28912
|
+
this.requestUpdate();
|
|
28913
|
+
};
|
|
28914
|
+
if (this.model || this.loadChildren) {
|
|
28915
|
+
if (this.#expandTimer !== void 0) clearTimeout(this.#expandTimer);
|
|
28916
|
+
this.#expandTimer = setTimeout(() => {
|
|
28917
|
+
this.#expandTimer = void 0;
|
|
28918
|
+
if (this.#pendingNode !== node) return;
|
|
28919
|
+
(this.model ? this.model.getChildren(node, nodeDepth - 1, controller.signal) : collectDataResult(this.loadChildren({
|
|
28920
|
+
node,
|
|
28921
|
+
depth: nodeDepth - 1
|
|
28922
|
+
}, controller.signal), controller.signal)).then((children) => applyChildren([...children])).catch(handleError);
|
|
28923
|
+
}, EXPAND_DEBOUNCE_MS);
|
|
28924
|
+
} else {
|
|
28925
|
+
this._loadingDepth = void 0;
|
|
28926
|
+
this.#pendingNode = void 0;
|
|
28927
|
+
this.#pendingController = void 0;
|
|
28928
|
+
const newColumns = this._columns.slice(0, nodeDepth);
|
|
28929
|
+
newColumns.push([]);
|
|
28930
|
+
this._columns = newColumns;
|
|
28931
|
+
}
|
|
28932
|
+
}
|
|
26211
28933
|
#onCheckboxClick(node) {
|
|
26212
28934
|
const currentlySelected = this._selectedKeys.has(node.item.key);
|
|
26213
28935
|
if (!!node.children?.length ? this.#allDescendantsSelected(node) : currentlySelected) this.#applyCheckedToggle(node, false);
|
|
@@ -26277,7 +28999,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26277
28999
|
#onResizeMove;
|
|
26278
29000
|
#onResizeEnd;
|
|
26279
29001
|
#getColumnHeader(depth) {
|
|
26280
|
-
if (depth === 0) return this
|
|
29002
|
+
if (depth === 0) return this.#displayRoot?.item.label ?? ``;
|
|
26281
29003
|
const explicit = this._selectedByDepth.get(depth - 1);
|
|
26282
29004
|
if (explicit) return explicit.item.label;
|
|
26283
29005
|
const prevCol = this._columns[depth - 1];
|
|
@@ -26288,9 +29010,11 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26288
29010
|
}
|
|
26289
29011
|
return ``;
|
|
26290
29012
|
}
|
|
26291
|
-
#columnStyle(depth) {
|
|
29013
|
+
#columnStyle(depth, isLast) {
|
|
26292
29014
|
const w = this.#columnWidths.get(depth);
|
|
26293
|
-
|
|
29015
|
+
if (w !== void 0) return `width:${w}px;flex-grow:0;flex-shrink:0;flex-basis:auto;max-width:none`;
|
|
29016
|
+
if (this.stretchLastColumn && isLast) return `width:max-content;flex-grow:1;flex-shrink:0;flex-basis:auto;min-width:0`;
|
|
29017
|
+
return `width:max-content;flex-grow:0;flex-shrink:0;flex-basis:auto`;
|
|
26294
29018
|
}
|
|
26295
29019
|
#isFocused(node, depth) {
|
|
26296
29020
|
return this.tickled.isTickled(this.#buildTickledId(node, depth));
|
|
@@ -26303,8 +29027,9 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26303
29027
|
}
|
|
26304
29028
|
#renderLoadingColumn() {
|
|
26305
29029
|
const depth = this._loadingDepth;
|
|
29030
|
+
const stretched = this.stretchLastColumn && this.#columnWidths.get(depth) === void 0;
|
|
26306
29031
|
return b`
|
|
26307
|
-
<div class="column loading-column" style=${this.#columnStyle(depth)}>
|
|
29032
|
+
<div class="column loading-column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, true)}>
|
|
26308
29033
|
${this.headers ? b`
|
|
26309
29034
|
<div class="column-header">${this.#getColumnHeader(depth)}</div>
|
|
26310
29035
|
` : A}
|
|
@@ -26319,8 +29044,10 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26319
29044
|
<div id="container" @keydown=${this.#onKeyDown} tabindex="0">
|
|
26320
29045
|
${this._columns.map((nodes, depth) => {
|
|
26321
29046
|
const visibleNodes = this.filterPredicate ? nodes.filter((n) => this.filterPredicate(n, depth)) : nodes;
|
|
29047
|
+
const isLast = depth === this._columns.length - 1 && this._loadingDepth === void 0;
|
|
29048
|
+
const stretched = this.stretchLastColumn && isLast && this.#columnWidths.get(depth) === void 0;
|
|
26322
29049
|
return b`
|
|
26323
|
-
<div class="column" style=${this.#columnStyle(depth)}>
|
|
29050
|
+
<div class="column ${stretched ? `stretch-last` : ``}" style=${this.#columnStyle(depth, isLast)}>
|
|
26324
29051
|
${this.headers ? b`
|
|
26325
29052
|
<div class="column-header"
|
|
26326
29053
|
@click=${() => this.#onHeaderClick(depth)}
|
|
@@ -26339,6 +29066,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26339
29066
|
})}
|
|
26340
29067
|
${this._loadingDepth !== void 0 ? this.#renderLoadingColumn() : A}
|
|
26341
29068
|
</div>
|
|
29069
|
+
${this.search.renderField()}
|
|
26342
29070
|
`;
|
|
26343
29071
|
}
|
|
26344
29072
|
_getFocusedIndex(depth) {
|
|
@@ -26374,6 +29102,7 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26374
29102
|
this.styles = [
|
|
26375
29103
|
themeFallbacks,
|
|
26376
29104
|
tickledStyles,
|
|
29105
|
+
listSearchStyles,
|
|
26377
29106
|
i$3`
|
|
26378
29107
|
:host {
|
|
26379
29108
|
--column-bg: var(--surface-3);
|
|
@@ -26408,6 +29137,20 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26408
29137
|
background: var(--column-bg);
|
|
26409
29138
|
border-right: 1px solid var(--column-separator);
|
|
26410
29139
|
flex-shrink: 0;
|
|
29140
|
+
/* Content-sized columns never grow wider than 80% of the available
|
|
29141
|
+
width — pathological labels truncate with ellipsis instead of
|
|
29142
|
+
pushing every other column out of view */
|
|
29143
|
+
max-width: 80%;
|
|
29144
|
+
/* Smoothly animate the last column stretching to fill remaining
|
|
29145
|
+
space and shrinking back to content width when it stops being last */
|
|
29146
|
+
transition: flex-grow var(--transition, 0.2s ease-out);
|
|
29147
|
+
}
|
|
29148
|
+
|
|
29149
|
+
.column.stretch-last {
|
|
29150
|
+
min-width: 0;
|
|
29151
|
+
/* Stretching fills remaining space by definition, so it is exempt
|
|
29152
|
+
from the 80% auto-size cap */
|
|
29153
|
+
max-width: none;
|
|
26411
29154
|
}
|
|
26412
29155
|
|
|
26413
29156
|
.column-header {
|
|
@@ -26491,6 +29234,12 @@ let MillerBaseElement = class MillerBaseElement extends i {
|
|
|
26491
29234
|
__decorate([property({ type: Object })], MillerBaseElement.prototype, "root", void 0);
|
|
26492
29235
|
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "model", void 0);
|
|
26493
29236
|
__decorate([property({ type: Boolean })], MillerBaseElement.prototype, "headers", void 0);
|
|
29237
|
+
__decorate([property({
|
|
29238
|
+
type: Boolean,
|
|
29239
|
+
attribute: `stretch-last-column`,
|
|
29240
|
+
reflect: true
|
|
29241
|
+
})], MillerBaseElement.prototype, "stretchLastColumn", void 0);
|
|
29242
|
+
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "viewRoot", void 0);
|
|
26494
29243
|
__decorate([property({ type: String })], MillerBaseElement.prototype, "interactionMode", void 0);
|
|
26495
29244
|
__decorate([property({
|
|
26496
29245
|
reflect: true,
|
|
@@ -26502,6 +29251,11 @@ __decorate([property({
|
|
|
26502
29251
|
})], MillerBaseElement.prototype, "exclusivity", void 0);
|
|
26503
29252
|
__decorate([property({ type: Object })], MillerBaseElement.prototype, "loadChildren", void 0);
|
|
26504
29253
|
__decorate([property({ attribute: false })], MillerBaseElement.prototype, "filterPredicate", void 0);
|
|
29254
|
+
__decorate([property({
|
|
29255
|
+
type: String,
|
|
29256
|
+
attribute: `search-modes`,
|
|
29257
|
+
reflect: true
|
|
29258
|
+
})], MillerBaseElement.prototype, "searchModes", void 0);
|
|
26505
29259
|
__decorate([state()], MillerBaseElement.prototype, "_columns", void 0);
|
|
26506
29260
|
__decorate([state()], MillerBaseElement.prototype, "_loadingDepth", void 0);
|
|
26507
29261
|
__decorate([state()], MillerBaseElement.prototype, "_selectedNodes", void 0);
|
|
@@ -26739,14 +29493,28 @@ var MillerTreeController = class {
|
|
|
26739
29493
|
* Any ancestor `MillerNode`s with unloaded children are populated
|
|
26740
29494
|
* synchronously before calling `element.navigateTo()`.
|
|
26741
29495
|
*
|
|
29496
|
+
* Key paths are relative to the element's current view root: when the view
|
|
29497
|
+
* is rebased, only matches at or below the view root can be reached.
|
|
29498
|
+
*
|
|
26742
29499
|
* Returns `false` if no matching node is found.
|
|
26743
29500
|
*/
|
|
26744
29501
|
navigateToValue(predicate) {
|
|
26745
29502
|
const treePath = this.#findTreePath(this.#root, predicate);
|
|
26746
29503
|
if (!treePath) return false;
|
|
26747
|
-
|
|
29504
|
+
const base = this.#element.viewRoot ?? this.#element.root;
|
|
29505
|
+
if (!base) return false;
|
|
29506
|
+
let startIndex = 1;
|
|
29507
|
+
if (this.#element.viewRoot) {
|
|
29508
|
+
const mappedBase = this.#nodeToTree.get(base);
|
|
29509
|
+
if (!mappedBase) return false;
|
|
29510
|
+
const baseIndex = treePath.indexOf(mappedBase);
|
|
29511
|
+
if (baseIndex < 0) return false;
|
|
29512
|
+
startIndex = baseIndex + 1;
|
|
29513
|
+
if (base.children === void 0) base.children = this.#childrenOf(mappedBase);
|
|
29514
|
+
}
|
|
29515
|
+
let currentColumn = base.children ?? [];
|
|
26748
29516
|
const keys = [];
|
|
26749
|
-
for (let i =
|
|
29517
|
+
for (let i = startIndex; i < treePath.length; i++) {
|
|
26750
29518
|
const treeNode = treePath[i];
|
|
26751
29519
|
const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
|
|
26752
29520
|
keys.push(key);
|
|
@@ -27228,6 +29996,136 @@ function dismiss(id) {
|
|
|
27228
29996
|
syncRenderer();
|
|
27229
29997
|
}
|
|
27230
29998
|
//#endregion
|
|
29999
|
+
//#region src/pan-text/pan-text.ts
|
|
30000
|
+
const SPEED = 45;
|
|
30001
|
+
const END_PADDING = 4;
|
|
30002
|
+
const START_DELAY = 300;
|
|
30003
|
+
const PAN_EASING = `cubic-bezier(0.25, 0.46, 0.45, 0.94)`;
|
|
30004
|
+
let PanTextElement = class PanTextElement extends i {
|
|
30005
|
+
constructor(..._args) {
|
|
30006
|
+
super(..._args);
|
|
30007
|
+
this.direction = `end`;
|
|
30008
|
+
this.disabled = false;
|
|
30009
|
+
this._isOverflowing = false;
|
|
30010
|
+
this._isHovering = false;
|
|
30011
|
+
this._overflow = 0;
|
|
30012
|
+
this._resizeObserver = null;
|
|
30013
|
+
this._onMouseEnter = () => {
|
|
30014
|
+
this._isHovering = true;
|
|
30015
|
+
};
|
|
30016
|
+
this._onMouseLeave = () => {
|
|
30017
|
+
this._isHovering = false;
|
|
30018
|
+
};
|
|
30019
|
+
this._measure = () => {
|
|
30020
|
+
if (this._isHovering) return;
|
|
30021
|
+
const el = this._textEl;
|
|
30022
|
+
if (!el) return;
|
|
30023
|
+
this._overflow = Math.max(0, el.scrollWidth - el.clientWidth);
|
|
30024
|
+
const overflowing = this._overflow > 0;
|
|
30025
|
+
this.classList.toggle(`is-overflowing`, overflowing);
|
|
30026
|
+
if (overflowing !== this._isOverflowing) this._isOverflowing = overflowing;
|
|
30027
|
+
};
|
|
30028
|
+
}
|
|
30029
|
+
connectedCallback() {
|
|
30030
|
+
super.connectedCallback();
|
|
30031
|
+
this._resizeObserver = new ResizeObserver(() => this._measure());
|
|
30032
|
+
this.addEventListener(`mouseenter`, this._onMouseEnter);
|
|
30033
|
+
this.addEventListener(`mouseleave`, this._onMouseLeave);
|
|
30034
|
+
}
|
|
30035
|
+
disconnectedCallback() {
|
|
30036
|
+
super.disconnectedCallback();
|
|
30037
|
+
this._resizeObserver?.disconnect();
|
|
30038
|
+
this._resizeObserver = null;
|
|
30039
|
+
this.removeEventListener(`mouseenter`, this._onMouseEnter);
|
|
30040
|
+
this.removeEventListener(`mouseleave`, this._onMouseLeave);
|
|
30041
|
+
}
|
|
30042
|
+
firstUpdated() {
|
|
30043
|
+
this._measure();
|
|
30044
|
+
this._resizeObserver?.observe(this);
|
|
30045
|
+
document.fonts?.ready.then(() => this._measure());
|
|
30046
|
+
}
|
|
30047
|
+
updated() {
|
|
30048
|
+
this._measure();
|
|
30049
|
+
}
|
|
30050
|
+
_textStyle() {
|
|
30051
|
+
if (!this._isOverflowing) return ``;
|
|
30052
|
+
const distance = this._overflow + END_PADDING;
|
|
30053
|
+
const duration = Math.max(1.5, distance / SPEED);
|
|
30054
|
+
const revealEnd = this.direction === `end`;
|
|
30055
|
+
const restX = revealEnd ? 0 : -distance;
|
|
30056
|
+
const hoverX = revealEnd ? -distance : 0;
|
|
30057
|
+
if (this._isHovering) return [
|
|
30058
|
+
`transform: translateX(${hoverX}px)`,
|
|
30059
|
+
`transition: transform ${duration}s ${PAN_EASING} ${START_DELAY}ms`,
|
|
30060
|
+
`overflow: visible`,
|
|
30061
|
+
`text-overflow: clip`
|
|
30062
|
+
].join(`; `);
|
|
30063
|
+
return [
|
|
30064
|
+
`transform: translateX(${restX}px)`,
|
|
30065
|
+
`transition: transform 400ms ${PAN_EASING}`,
|
|
30066
|
+
revealEnd ? `` : `overflow: visible; text-overflow: clip`
|
|
30067
|
+
].filter(Boolean).join(`; `);
|
|
30068
|
+
}
|
|
30069
|
+
render() {
|
|
30070
|
+
const textClasses = {
|
|
30071
|
+
"is-overflowing": this._isOverflowing,
|
|
30072
|
+
"is-hovering": this._isHovering
|
|
30073
|
+
};
|
|
30074
|
+
return b`
|
|
30075
|
+
<span class="text ${classMap(textClasses)}" part="text" style=${this._textStyle()}>
|
|
30076
|
+
<slot @slotchange=${this._measure}></slot>
|
|
30077
|
+
</span>
|
|
30078
|
+
`;
|
|
30079
|
+
}
|
|
30080
|
+
static {
|
|
30081
|
+
this.styles = [themeFallbacks, i$3`
|
|
30082
|
+
/* Renders like plain inline text: no colours, border, padding or margin
|
|
30083
|
+
of its own — it only adds the clipping needed to pan on hover. */
|
|
30084
|
+
:host {
|
|
30085
|
+
display: inline-block;
|
|
30086
|
+
overflow: hidden;
|
|
30087
|
+
white-space: nowrap;
|
|
30088
|
+
max-width: 100%;
|
|
30089
|
+
box-sizing: border-box;
|
|
30090
|
+
font: inherit;
|
|
30091
|
+
color: inherit;
|
|
30092
|
+
}
|
|
30093
|
+
|
|
30094
|
+
:host([disabled]) {
|
|
30095
|
+
opacity: var(--disabled-opacity, 0.5);
|
|
30096
|
+
pointer-events: none;
|
|
30097
|
+
}
|
|
30098
|
+
|
|
30099
|
+
.text {
|
|
30100
|
+
display: block;
|
|
30101
|
+
overflow: hidden;
|
|
30102
|
+
text-overflow: ellipsis;
|
|
30103
|
+
white-space: nowrap;
|
|
30104
|
+
will-change: transform;
|
|
30105
|
+
}
|
|
30106
|
+
|
|
30107
|
+
@media (prefers-reduced-motion: reduce) {
|
|
30108
|
+
.text {
|
|
30109
|
+
transition: none !important;
|
|
30110
|
+
transform: none !important;
|
|
30111
|
+
}
|
|
30112
|
+
}
|
|
30113
|
+
`];
|
|
30114
|
+
}
|
|
30115
|
+
};
|
|
30116
|
+
__decorate([property({
|
|
30117
|
+
type: String,
|
|
30118
|
+
attribute: `direction`
|
|
30119
|
+
})], PanTextElement.prototype, "direction", void 0);
|
|
30120
|
+
__decorate([property({
|
|
30121
|
+
type: Boolean,
|
|
30122
|
+
attribute: `disabled`
|
|
30123
|
+
})], PanTextElement.prototype, "disabled", void 0);
|
|
30124
|
+
__decorate([state()], PanTextElement.prototype, "_isOverflowing", void 0);
|
|
30125
|
+
__decorate([state()], PanTextElement.prototype, "_isHovering", void 0);
|
|
30126
|
+
__decorate([query(`.text`)], PanTextElement.prototype, "_textEl", void 0);
|
|
30127
|
+
PanTextElement = __decorate([safeCustomElement(`ixfx-pan-text`)], PanTextElement);
|
|
30128
|
+
//#endregion
|
|
27231
30129
|
//#region src/panel/panel-group.ts
|
|
27232
30130
|
let PanelGroupElement = class PanelGroupElement extends i {
|
|
27233
30131
|
constructor(..._args) {
|
|
@@ -35444,29 +38342,24 @@ var TreeController = class {
|
|
|
35444
38342
|
navigateToValue(predicate) {
|
|
35445
38343
|
const treePath = this.#findTreePath(this.#root, predicate);
|
|
35446
38344
|
if (!treePath) return false;
|
|
38345
|
+
let currentColumn = this.#element.root?.children ?? [];
|
|
35447
38346
|
const keys = [];
|
|
35448
38347
|
for (let i = 1; i < treePath.length; i++) {
|
|
35449
38348
|
const treeNode = treePath[i];
|
|
35450
38349
|
const key = this.#options.valueToItem(treeNode.getValue(), treeNode).key;
|
|
35451
38350
|
keys.push(key);
|
|
35452
38351
|
if (i < treePath.length - 1) {
|
|
35453
|
-
const
|
|
35454
|
-
const treeNodeMap = this.#nodeToTree;
|
|
35455
|
-
const node = currentChildren.find((n) => n.item.key === key);
|
|
38352
|
+
const node = currentColumn.find((n) => n.item.key === key);
|
|
35456
38353
|
if (!node) return false;
|
|
35457
38354
|
if (node.children === void 0) {
|
|
35458
|
-
const mapped =
|
|
38355
|
+
const mapped = this.#nodeToTree.get(node);
|
|
35459
38356
|
if (mapped) node.children = this.#childrenOf(mapped);
|
|
35460
38357
|
}
|
|
38358
|
+
currentColumn = node.children ?? [];
|
|
35461
38359
|
}
|
|
35462
38360
|
}
|
|
35463
38361
|
return this.#element.navigateTo(keys);
|
|
35464
38362
|
}
|
|
35465
|
-
#findInChildren(children, keys) {
|
|
35466
|
-
if (keys.length === 0) return children;
|
|
35467
|
-
const key = keys[0];
|
|
35468
|
-
return children.find((n) => n.item.key === key)?.children ?? [];
|
|
35469
|
-
}
|
|
35470
38363
|
#childrenOf(treeNode) {
|
|
35471
38364
|
return Array.from(treeNode.children(), (child) => this.#toTreeNode(child));
|
|
35472
38365
|
}
|
|
@@ -36631,6 +39524,6 @@ function init() {
|
|
|
36631
39524
|
console.log(`Init`);
|
|
36632
39525
|
}
|
|
36633
39526
|
//#endregion
|
|
36634
|
-
export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, readBaseColour, registerFontIcon, registerIcon, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
|
|
39527
|
+
export { AcTextElement, AcTokenElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeKeyboardNavController, TreeListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, attachTimelineInteractions, buildHighlightTargets, buildModel, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeLeafGeoms, createAcCompletionProvider, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, extractStack, findNode, findParentOf, findStackOf, findStackPathByName, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normalize, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, walk, wrapRoot };
|
|
36635
39528
|
|
|
36636
39529
|
//# sourceMappingURL=index.js.map
|