@ixfx/components 0.10.6 → 0.10.8
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 +410 -2
- package/bundle/index.d.ts.map +1 -1
- package/bundle/index.js +1583 -156
- package/bundle/index.js.map +1 -1
- package/dist/crumbs.d.ts +2 -2
- package/dist/{data-grid-DE1_gZNk.d.ts → data-grid-3yIylIH-.d.ts} +8 -6
- package/dist/data-grid-3yIylIH-.d.ts.map +1 -0
- package/dist/data-grid.d.ts +3 -3
- package/dist/data-grid.js +10 -5
- package/dist/data-grid.js.map +1 -1
- package/dist/{detail-list-CaStSzYN.d.ts → detail-list-ccWFyhEc.d.ts} +4 -4
- package/dist/{detail-list-CaStSzYN.d.ts.map → detail-list-ccWFyhEc.d.ts.map} +1 -1
- package/dist/detail-list.d.ts +3 -3
- package/dist/detail-list.js +49 -1
- package/dist/detail-list.js.map +1 -1
- package/dist/{grid-list-BJDxBL0x.d.ts → grid-list-BnOVgtLI.d.ts} +4 -4
- package/dist/{grid-list-BJDxBL0x.d.ts.map → grid-list-BnOVgtLI.d.ts.map} +1 -1
- package/dist/{grid-list-CaMcdpFJ.js → grid-list-k89JpiNy.js} +18 -1
- package/dist/grid-list-k89JpiNy.js.map +1 -0
- package/dist/grid-list.d.ts +2 -2
- package/dist/grid-list.js +1 -1
- package/dist/incr-search.d.ts +1 -1
- package/dist/{index-CsaQwNkz.d.ts → index-5-cwYpmx.d.ts} +3 -3
- package/dist/{index-CsaQwNkz.d.ts.map → index-5-cwYpmx.d.ts.map} +1 -1
- package/dist/{index-9AhaHIT2.d.ts → index-RImMNo0r.d.ts} +2 -2
- package/dist/{index-9AhaHIT2.d.ts.map → index-RImMNo0r.d.ts.map} +1 -1
- package/dist/index.d.ts +409 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1365 -50
- package/dist/index.js.map +1 -1
- package/dist/{list-native-drag-controller-BAuWAtyQ.d.ts → list-native-drag-controller-B9reY4uM.d.ts} +2 -2
- package/dist/{list-native-drag-controller-BAuWAtyQ.d.ts.map → list-native-drag-controller-B9reY4uM.d.ts.map} +1 -1
- package/dist/menu.d.ts +1 -1
- package/dist/miller.d.ts +3 -3
- package/dist/{split-layout-BJEU1W0d.js → split-layout-CXotkdbx.js} +46 -4
- package/dist/split-layout-CXotkdbx.js.map +1 -0
- package/dist/split-layout-JK3xWVX2.d.ts.map +1 -1
- package/dist/split-layout.js +1 -1
- package/dist/{tree-component-DGmKBbpy.d.ts → tree-component-CMGeqyjZ.d.ts} +2 -2
- package/dist/{tree-component-DGmKBbpy.d.ts.map → tree-component-CMGeqyjZ.d.ts.map} +1 -1
- package/dist/tree.d.ts +4 -4
- package/dist/{types-Bc23SI94.d.ts → types-3Jfcaxvo.d.ts} +8 -1
- package/dist/types-3Jfcaxvo.d.ts.map +1 -0
- package/dist/{vertical-list-DfcNsa4p.d.ts → vertical-list-9dJ1R1hU.d.ts} +3 -3
- package/dist/{vertical-list-DfcNsa4p.d.ts.map → vertical-list-9dJ1R1hU.d.ts.map} +1 -1
- package/dist/vertical-list.d.ts +2 -2
- package/docs-user/README.md +1 -0
- package/docs-user/data-grid.md +13 -3
- package/docs-user/detail-list.md +31 -3
- package/docs-user/grid-list.md +4 -2
- package/docs-user/hyperbolic-tree.md +282 -0
- package/docs-user/index.json +7 -2
- package/docs-user/llms.txt +1 -0
- package/docs-user/multicol-list.md +4 -0
- package/docs-user/tree.md +1 -0
- package/llms.txt +1 -0
- package/package.json +1 -1
- package/dist/data-grid-DE1_gZNk.d.ts.map +0 -1
- package/dist/grid-list-CaMcdpFJ.js.map +0 -1
- package/dist/split-layout-BJEU1W0d.js.map +0 -1
- package/dist/types-Bc23SI94.d.ts.map +0 -1
package/bundle/index.js
CHANGED
|
@@ -14907,7 +14907,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
14907
14907
|
}
|
|
14908
14908
|
case ` `: {
|
|
14909
14909
|
if (this._editing) {
|
|
14910
|
-
if (this.columns.find((c) => c.key === this._editing.colKey)?.
|
|
14910
|
+
if ((this.columns.find((c) => c.key === this._editing.colKey)?.dataType ?? `text`) === `boolean`) {
|
|
14911
14911
|
e.preventDefault();
|
|
14912
14912
|
const isImmediate = this.editability === `immediate`;
|
|
14913
14913
|
const rowKey = this._editing.rowKey;
|
|
@@ -15424,11 +15424,13 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
15424
15424
|
if (!col) return rows;
|
|
15425
15425
|
const dir = this.sortState.direction === `asc` ? 1 : -1;
|
|
15426
15426
|
if (col.sortFn) return rows.sort((a, b) => dir * col.sortFn(a, b));
|
|
15427
|
+
const dataType = col.dataType ?? `text`;
|
|
15427
15428
|
return rows.sort((a, b) => {
|
|
15428
15429
|
const av = a[col.key];
|
|
15429
15430
|
const bv = b[col.key];
|
|
15430
15431
|
if (av === bv) return 0;
|
|
15431
|
-
if (
|
|
15432
|
+
if (dataType === `number`) return dir * (av - bv);
|
|
15433
|
+
if (dataType === `boolean`) return dir * (Boolean(av) === Boolean(bv) ? 0 : av ? 1 : -1);
|
|
15432
15434
|
return dir * String(av).localeCompare(String(bv));
|
|
15433
15435
|
});
|
|
15434
15436
|
}
|
|
@@ -15805,7 +15807,7 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
15805
15807
|
}
|
|
15806
15808
|
_isTextEditor(col) {
|
|
15807
15809
|
const e = col.editor;
|
|
15808
|
-
return !e || e === `
|
|
15810
|
+
return !e || e === `auto`;
|
|
15809
15811
|
}
|
|
15810
15812
|
_isCellEditing(rowKey, colKey) {
|
|
15811
15813
|
if (this.editability === `immediate` && this._isEditable(this._sortedRows.find((r, i) => this.getRowKey(r, i) === rowKey) ?? {}, this.columns.find((c) => c.key === colKey))) return true;
|
|
@@ -16247,7 +16249,8 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
16247
16249
|
if (this._isCellEditing(rowKey, col.key)) {
|
|
16248
16250
|
const immediate = this.editability === `immediate` || this._editingRow === rowKey;
|
|
16249
16251
|
const curVal = immediate ? val : this._editValue ?? val;
|
|
16250
|
-
|
|
16252
|
+
const effectiveDataType = col.dataType ?? `text`;
|
|
16253
|
+
if (effectiveDataType === `boolean` && (col.editor === `auto` || col.editor === `select` || !col.editor)) return b`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px;justify-content:center;align-items:center"><input type="checkbox" .checked=${Boolean(curVal)} @change=${(e) => {
|
|
16251
16254
|
const v = e.target.checked;
|
|
16252
16255
|
if (immediate) this._commitImmediate(rowKey, col.key, v);
|
|
16253
16256
|
else {
|
|
@@ -16305,7 +16308,9 @@ let DataGridElement = class DataGridElement extends i {
|
|
|
16305
16308
|
};
|
|
16306
16309
|
return b`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px">${factory(ctx)}</div>`;
|
|
16307
16310
|
}
|
|
16308
|
-
|
|
16311
|
+
const inputType = effectiveDataType === `number` ? `number` : `text`;
|
|
16312
|
+
const inputValue = effectiveDataType === `number` ? String(curVal ?? ``) : String(curVal ?? ``);
|
|
16313
|
+
return b`<div class="cell" data-col=${col.key} style="width:${col.width ?? 150}px"><input type=${inputType} .value=${inputValue} @input=${(e) => this._editValue = e.target.value} @keydown=${(e) => {
|
|
16309
16314
|
if (e.key === `Enter`) this._commitEdit();
|
|
16310
16315
|
if (e.key === `Escape`) {
|
|
16311
16316
|
this._editing = null;
|
|
@@ -16709,6 +16714,23 @@ DataTableElement = __decorate([safeCustomElement(`ixfx-data-table`)], DataTableE
|
|
|
16709
16714
|
const ITEM_SELECTOR$3 = `:scope > li`;
|
|
16710
16715
|
const SEARCH_HIGHLIGHT_KEY = `detail-list-search`;
|
|
16711
16716
|
const LABEL_SPAN_CSS = `overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0`;
|
|
16717
|
+
function pointInRect(x, y, rect) {
|
|
16718
|
+
return rect.width > 0 && rect.height > 0 && x >= rect.left && x < rect.right && y >= rect.top && y < rect.bottom;
|
|
16719
|
+
}
|
|
16720
|
+
/** Hit-test the painted content, not a layout wrapper that may fill the row. */
|
|
16721
|
+
function contentAtPoint(node, x, y) {
|
|
16722
|
+
if (node instanceof Text) {
|
|
16723
|
+
if (!node.textContent?.trim()) return false;
|
|
16724
|
+
const range = document.createRange();
|
|
16725
|
+
range.selectNodeContents(node);
|
|
16726
|
+
return Array.from(range.getClientRects()).some((rect) => pointInRect(x, y, rect));
|
|
16727
|
+
}
|
|
16728
|
+
if (!(node instanceof Element)) return false;
|
|
16729
|
+
if (node.hasAttribute(`data-drag-select-blank`)) return false;
|
|
16730
|
+
if (node.hasAttribute(`data-drag-select-content`)) return pointInRect(x, y, node.getBoundingClientRect());
|
|
16731
|
+
if (Array.from(node.childNodes).some((child) => contentAtPoint(child, x, y))) return true;
|
|
16732
|
+
return node.childNodes.length === 0 && pointInRect(x, y, node.getBoundingClientRect());
|
|
16733
|
+
}
|
|
16712
16734
|
/** Shallow key-array equality, treating `undefined` (never dispatched) as different. */
|
|
16713
16735
|
function sameKeys$2(a, b) {
|
|
16714
16736
|
if (a === void 0 || a.length !== b.length) return false;
|
|
@@ -16769,6 +16791,10 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
16769
16791
|
});
|
|
16770
16792
|
this.#nativeDragging = false;
|
|
16771
16793
|
this.#postDragSuppressTimer = 0;
|
|
16794
|
+
this.#blankItemPressPending = false;
|
|
16795
|
+
this.#clearBlankItemPress = () => {
|
|
16796
|
+
this.#blankItemPressPending = false;
|
|
16797
|
+
};
|
|
16772
16798
|
this.#nativeDrag = new ListNativeDragController(this, {
|
|
16773
16799
|
source: `detail-list`,
|
|
16774
16800
|
isEnabled: () => this.itemsDraggable && this.selectionMode !== `none` && this._getVisibleItems().length > 0,
|
|
@@ -16800,7 +16826,8 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
16800
16826
|
for (const el of this.querySelectorAll(`li`)) el.removeAttribute(`data-drag-origin`);
|
|
16801
16827
|
this.#startPostDragSuppressTimer();
|
|
16802
16828
|
},
|
|
16803
|
-
syncLightDomDraggable: () => this._syncNativeDraggable()
|
|
16829
|
+
syncLightDomDraggable: () => this._syncNativeDraggable(),
|
|
16830
|
+
shouldStart: () => !this.#blankItemPressPending
|
|
16804
16831
|
});
|
|
16805
16832
|
this.#dragSelect = new ListDragSelectController(this, {
|
|
16806
16833
|
getSurface: () => this.renderRoot.querySelector(`.container`),
|
|
@@ -16820,6 +16847,10 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
16820
16847
|
},
|
|
16821
16848
|
autoScroll: (dx, _dy) => {
|
|
16822
16849
|
if (this._listEl) this._listEl.scrollLeft += dx;
|
|
16850
|
+
},
|
|
16851
|
+
shouldStart: (event) => {
|
|
16852
|
+
this.#blankItemPressPending = this.itemsDraggable && this.#isBlankItemPress(event);
|
|
16853
|
+
return !this.itemsDraggable || this.#blankItemPressPending;
|
|
16823
16854
|
}
|
|
16824
16855
|
});
|
|
16825
16856
|
this.search = new ListSearchController(this, { adapter: createElementListSearchAdapter(this, {
|
|
@@ -16938,6 +16969,8 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
16938
16969
|
#data;
|
|
16939
16970
|
#nativeDragging;
|
|
16940
16971
|
#postDragSuppressTimer;
|
|
16972
|
+
#blankItemPressPending;
|
|
16973
|
+
#clearBlankItemPress;
|
|
16941
16974
|
#nativeDrag;
|
|
16942
16975
|
#prepareNativeDrag(originKey) {
|
|
16943
16976
|
const visible = this._getVisibleItems();
|
|
@@ -16975,6 +17008,21 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
16975
17008
|
/** Live drag-select preview rect (surface-relative); the covered items carry `data-marquee`. */
|
|
16976
17009
|
#marqueeRect;
|
|
16977
17010
|
#dragSelect;
|
|
17011
|
+
/** A row's padding/empty area may start marquee; its actual content may drag out. */
|
|
17012
|
+
#isBlankItemPress(event) {
|
|
17013
|
+
const path = event.composedPath();
|
|
17014
|
+
const item = this._findListItem(path);
|
|
17015
|
+
if (!item) return false;
|
|
17016
|
+
for (const node of path) {
|
|
17017
|
+
if (node === item) break;
|
|
17018
|
+
if (node instanceof Element) {
|
|
17019
|
+
if (node.matches(`input, button, a, select, textarea, [contenteditable], .ixfx-list-checkbox, [role="checkbox"]`)) return false;
|
|
17020
|
+
if (node.hasAttribute(`data-drag-select-blank`)) return true;
|
|
17021
|
+
if (node.hasAttribute(`data-drag-select-content`)) return false;
|
|
17022
|
+
}
|
|
17023
|
+
}
|
|
17024
|
+
return !Array.from(item.childNodes).some((node) => contentAtPoint(node, event.clientX, event.clientY));
|
|
17025
|
+
}
|
|
16978
17026
|
/** Collect visible items whose surface-relative box intersects `rect`. */
|
|
16979
17027
|
#marqueeHitTest(rect) {
|
|
16980
17028
|
const surface = this.renderRoot.querySelector(`.container`);
|
|
@@ -17023,6 +17071,8 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
17023
17071
|
this.addEventListener(`click`, this._onClick);
|
|
17024
17072
|
this.addEventListener(`dblclick`, this._onDblClick);
|
|
17025
17073
|
this.addEventListener(`wheel`, this._onWheel);
|
|
17074
|
+
window.addEventListener(`pointerup`, this.#clearBlankItemPress);
|
|
17075
|
+
window.addEventListener(`pointercancel`, this.#clearBlankItemPress);
|
|
17026
17076
|
this.#resizeObserver = new ResizeObserver(() => this.#scheduleGeometry());
|
|
17027
17077
|
this.#resizeObserver.observe(this);
|
|
17028
17078
|
this.#mutationObserver = new MutationObserver((records) => {
|
|
@@ -17067,6 +17117,9 @@ let DetailListElement = class DetailListElement extends i {
|
|
|
17067
17117
|
this.removeEventListener(`click`, this._onClick);
|
|
17068
17118
|
this.removeEventListener(`dblclick`, this._onDblClick);
|
|
17069
17119
|
this.removeEventListener(`wheel`, this._onWheel);
|
|
17120
|
+
window.removeEventListener(`pointerup`, this.#clearBlankItemPress);
|
|
17121
|
+
window.removeEventListener(`pointercancel`, this.#clearBlankItemPress);
|
|
17122
|
+
this.#blankItemPressPending = false;
|
|
17070
17123
|
cancelAnimationFrame(this.#sortRaf);
|
|
17071
17124
|
cancelAnimationFrame(this.#viewportScrollRaf);
|
|
17072
17125
|
this.#viewportScrollRaf = 0;
|
|
@@ -19080,6 +19133,44 @@ function collectWeights(snapshot, weights, collapsed) {
|
|
|
19080
19133
|
if (snapshot.collapsed !== void 0) collapsed.set(snapshot.id, snapshot.collapsed);
|
|
19081
19134
|
} else if (snapshot.children) for (const child of snapshot.children) collectWeights(child, weights, collapsed);
|
|
19082
19135
|
}
|
|
19136
|
+
function setPanelRestoreWeight(group, id, restoreWeight) {
|
|
19137
|
+
const children = group.children.map((child) => {
|
|
19138
|
+
if (child.kind === `panel`) return child.id === id ? {
|
|
19139
|
+
...child,
|
|
19140
|
+
restoreWeight
|
|
19141
|
+
} : child;
|
|
19142
|
+
return setPanelRestoreWeight(child, id, restoreWeight);
|
|
19143
|
+
});
|
|
19144
|
+
return {
|
|
19145
|
+
...group,
|
|
19146
|
+
children
|
|
19147
|
+
};
|
|
19148
|
+
}
|
|
19149
|
+
function preserveRestoreWeights(previous, next) {
|
|
19150
|
+
if (!previous) return next;
|
|
19151
|
+
const previousById = new Map(previous.children.map((child) => [child.id, child]));
|
|
19152
|
+
const children = next.children.map((child) => {
|
|
19153
|
+
const match = previousById.get(child.id);
|
|
19154
|
+
if (!match || match.kind !== child.kind) return child;
|
|
19155
|
+
if (child.kind === `panel` && match.kind === `panel`) return {
|
|
19156
|
+
...child,
|
|
19157
|
+
restoreWeight: match.restoreWeight
|
|
19158
|
+
};
|
|
19159
|
+
return preserveRestoreWeights(match, child);
|
|
19160
|
+
});
|
|
19161
|
+
return {
|
|
19162
|
+
...next,
|
|
19163
|
+
children
|
|
19164
|
+
};
|
|
19165
|
+
}
|
|
19166
|
+
function findPanelNode(group, id) {
|
|
19167
|
+
for (const child of group.children) if (child.kind === `panel`) {
|
|
19168
|
+
if (child.id === id) return child;
|
|
19169
|
+
} else {
|
|
19170
|
+
const found = findPanelNode(child, id);
|
|
19171
|
+
if (found) return found;
|
|
19172
|
+
}
|
|
19173
|
+
}
|
|
19083
19174
|
function walk(group, visitor) {
|
|
19084
19175
|
walkInner(group, [], visitor);
|
|
19085
19176
|
}
|
|
@@ -19212,7 +19303,7 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
|
|
|
19212
19303
|
_rebuildModel() {
|
|
19213
19304
|
this._reindexSlots();
|
|
19214
19305
|
const normalized = normalise(buildModel(this));
|
|
19215
|
-
this._model = normalized;
|
|
19306
|
+
this._model = preserveRestoreWeights(this._model, normalized);
|
|
19216
19307
|
this.requestUpdate();
|
|
19217
19308
|
}
|
|
19218
19309
|
get panels() {
|
|
@@ -19334,6 +19425,7 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
|
|
|
19334
19425
|
if (!panel || panel.collapsed === collapsed) return;
|
|
19335
19426
|
this._applyingOwnMutation = true;
|
|
19336
19427
|
if (collapsed) {
|
|
19428
|
+
if (this._model) this._model = setPanelRestoreWeight(this._model, panel.id, panel.weight);
|
|
19337
19429
|
panel.el.setAttribute(`collapsed`, ``);
|
|
19338
19430
|
panel.el.setAttribute(`data-collapsed`, ``);
|
|
19339
19431
|
} else {
|
|
@@ -19341,7 +19433,10 @@ let SplitLayoutElement = class SplitLayoutElement extends i {
|
|
|
19341
19433
|
panel.el.removeAttribute(`data-collapsed`);
|
|
19342
19434
|
}
|
|
19343
19435
|
this._rebuildModel();
|
|
19344
|
-
if (!collapsed
|
|
19436
|
+
if (!collapsed) {
|
|
19437
|
+
const restoreWeight = revealWeight ?? (this._model ? findPanelNode(this._model, panel.id)?.restoreWeight : void 0);
|
|
19438
|
+
if (restoreWeight !== void 0) this._setFlexibleWeight(panel.id, restoreWeight);
|
|
19439
|
+
}
|
|
19345
19440
|
this.requestUpdate();
|
|
19346
19441
|
this._observer?.takeRecords();
|
|
19347
19442
|
this._applyingOwnMutation = false;
|
|
@@ -20934,6 +21029,7 @@ let DockHostElement = class DockHostElement extends i {
|
|
|
20934
21029
|
this._junctionsKey = ``;
|
|
20935
21030
|
this._junctionsRaf = null;
|
|
20936
21031
|
this._hostResizeObserver = null;
|
|
21032
|
+
this._loadGeneration = 0;
|
|
20937
21033
|
this._toolbarSlots = /* @__PURE__ */ new Set();
|
|
20938
21034
|
this._toolbarObserver = null;
|
|
20939
21035
|
this._panelObservers = /* @__PURE__ */ new Map();
|
|
@@ -21251,6 +21347,7 @@ let DockHostElement = class DockHostElement extends i {
|
|
|
21251
21347
|
}
|
|
21252
21348
|
loadLayout(layout) {
|
|
21253
21349
|
this._returnAllToolbars();
|
|
21350
|
+
this._loadGeneration++;
|
|
21254
21351
|
this._model = normalize(cloneNode(layout.root));
|
|
21255
21352
|
this._mergePoolIntoModel();
|
|
21256
21353
|
this.requestUpdate();
|
|
@@ -21952,12 +22049,18 @@ let DockHostElement = class DockHostElement extends i {
|
|
|
21952
22049
|
const splits = this.shadowRoot?.querySelectorAll(`ixfx-split-layout[data-dock-path]`);
|
|
21953
22050
|
if (splits === void 0) return;
|
|
21954
22051
|
for (const el of splits) {
|
|
22052
|
+
if (el.getAttribute(`data-dock-gen`) !== String(this._loadGeneration)) continue;
|
|
21955
22053
|
const pathStr = el.getAttribute(`data-dock-path`);
|
|
21956
22054
|
if (pathStr === null) continue;
|
|
21957
22055
|
const node = findNode(this._model, parsePath(pathStr));
|
|
21958
22056
|
if (node === null || !isSplit(node)) continue;
|
|
21959
22057
|
const live = Object.values(el.getSizes());
|
|
21960
|
-
if (live.length === node.children.length && !weightsEqual(live, node.sizes))
|
|
22058
|
+
if (live.length === node.children.length && !weightsEqual(live, node.sizes)) {
|
|
22059
|
+
node.sizes = live;
|
|
22060
|
+
Array.from(el.children).forEach((child, i) => {
|
|
22061
|
+
if (child instanceof HTMLElement && node.sizes[i] !== void 0) child.setAttribute(`size`, String(node.sizes[i]));
|
|
22062
|
+
});
|
|
22063
|
+
}
|
|
21961
22064
|
}
|
|
21962
22065
|
}
|
|
21963
22066
|
_teardownTabDragListeners() {
|
|
@@ -22086,6 +22189,7 @@ let DockHostElement = class DockHostElement extends i {
|
|
|
22086
22189
|
size=${size === void 0 ? A : String(size)}
|
|
22087
22190
|
style=${`--split-gutter-size: var(--dock-split-bar-size, 2px); --split-collapsed-size: var(--dock-collapsed-size, 40px)`}
|
|
22088
22191
|
data-dock-path=${pathStr}
|
|
22192
|
+
data-dock-gen=${this._loadGeneration}
|
|
22089
22193
|
@ixfx-split-resize=${this._onSplitWeightsChanged}
|
|
22090
22194
|
@ixfx-split-change=${this._onSplitWeightsChanged}
|
|
22091
22195
|
@ixfx-split-collapse=${(e) => this._onSplitCollapsed(e, true)}
|
|
@@ -25263,6 +25367,7 @@ let GridListElement = class GridListElement extends i {
|
|
|
25263
25367
|
data-input-mode=${this.tickled.inputMode}
|
|
25264
25368
|
@scroll=${this.#onScroll}
|
|
25265
25369
|
@wheel=${this.#onWheel}
|
|
25370
|
+
@click=${this.#onViewportClick}
|
|
25266
25371
|
>
|
|
25267
25372
|
<div class="spacer" style=${spacerStyle}>
|
|
25268
25373
|
${this.#source instanceof DataGridListSource ? this.#renderDataCells(win) : this.#renderElementCells(win)}
|
|
@@ -25495,6 +25600,22 @@ let GridListElement = class GridListElement extends i {
|
|
|
25495
25600
|
this.selection.handleClick(key, e);
|
|
25496
25601
|
this.#emitItemEvent(`list-item-click`, key);
|
|
25497
25602
|
}
|
|
25603
|
+
/**
|
|
25604
|
+
* A click that lands on the viewport without touching a cell — the empty
|
|
25605
|
+
* space between/around items — clears the selection (Finder-style). The
|
|
25606
|
+
* guard mirrors `#onCellClick`: a trailing click after a marquee or native
|
|
25607
|
+
* drag is swallowed so it cannot undo the selection that drag just made.
|
|
25608
|
+
* In `manual` interaction mode the consumer owns selection, so nothing is
|
|
25609
|
+
* cleared. Cell clicks route through `#onCellClick` and are skipped here.
|
|
25610
|
+
*/
|
|
25611
|
+
#onViewportClick(e) {
|
|
25612
|
+
const mode = this.interactionMode;
|
|
25613
|
+
if (mode === `manual`) return;
|
|
25614
|
+
if (this.selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
|
|
25615
|
+
if (this.#dragSelect.isDragging || this.#nativeDragging || this.#nativeDrag.isDragging || this.#postDragSuppressTimer !== 0) return;
|
|
25616
|
+
for (const node of e.composedPath()) if (node instanceof Element && node.classList.contains(`cell`)) return;
|
|
25617
|
+
this.selection.clear();
|
|
25618
|
+
}
|
|
25498
25619
|
#startPostDragSuppressTimer() {
|
|
25499
25620
|
if (this.#postDragSuppressTimer) clearTimeout(this.#postDragSuppressTimer);
|
|
25500
25621
|
this.#postDragSuppressTimer = window.setTimeout(() => {
|
|
@@ -31194,17 +31315,117 @@ __decorate([state()], IxfxHexEditorElement.prototype, "_shadeMap", void 0);
|
|
|
31194
31315
|
__decorate([state()], IxfxHexEditorElement.prototype, "_focusSide", void 0);
|
|
31195
31316
|
IxfxHexEditorElement = __decorate([safeCustomElement(`ixfx-hex-editor`)], IxfxHexEditorElement);
|
|
31196
31317
|
//#endregion
|
|
31197
|
-
//#region src/
|
|
31198
|
-
|
|
31199
|
-
function
|
|
31200
|
-
return
|
|
31318
|
+
//#region src/hyperbolic-tree/poincare-math.ts
|
|
31319
|
+
const EPSILON = 1e-9;
|
|
31320
|
+
function complexMul(a, b) {
|
|
31321
|
+
return {
|
|
31322
|
+
x: a.x * b.x - a.y * b.y,
|
|
31323
|
+
y: a.x * b.y + a.y * b.x
|
|
31324
|
+
};
|
|
31325
|
+
}
|
|
31326
|
+
function complexConjugate(a) {
|
|
31327
|
+
return {
|
|
31328
|
+
x: a.x,
|
|
31329
|
+
y: -a.y
|
|
31330
|
+
};
|
|
31331
|
+
}
|
|
31332
|
+
function complexAdd(a, b) {
|
|
31333
|
+
return {
|
|
31334
|
+
x: a.x + b.x,
|
|
31335
|
+
y: a.y + b.y
|
|
31336
|
+
};
|
|
31201
31337
|
}
|
|
31338
|
+
function complexSub(a, b) {
|
|
31339
|
+
return {
|
|
31340
|
+
x: a.x - b.x,
|
|
31341
|
+
y: a.y - b.y
|
|
31342
|
+
};
|
|
31343
|
+
}
|
|
31344
|
+
function complexScale(a, s) {
|
|
31345
|
+
return {
|
|
31346
|
+
x: a.x * s,
|
|
31347
|
+
y: a.y * s
|
|
31348
|
+
};
|
|
31349
|
+
}
|
|
31350
|
+
function complexDiv(a, b) {
|
|
31351
|
+
const denominator = b.x * b.x + b.y * b.y;
|
|
31352
|
+
if (denominator < EPSILON) return {
|
|
31353
|
+
x: 0,
|
|
31354
|
+
y: 0
|
|
31355
|
+
};
|
|
31356
|
+
return complexScale(complexMul(a, complexConjugate(b)), 1 / denominator);
|
|
31357
|
+
}
|
|
31358
|
+
/** Euclidean norm (`|z|`) of a point. */
|
|
31359
|
+
function norm(a) {
|
|
31360
|
+
return Math.hypot(a.x, a.y);
|
|
31361
|
+
}
|
|
31362
|
+
/** Clamp a point to stay strictly inside the unit disk (the Poincaré model's boundary is never reached). */
|
|
31363
|
+
function clampToDisk(p, maxNorm = .999999) {
|
|
31364
|
+
const n = norm(p);
|
|
31365
|
+
if (n <= maxNorm) return p;
|
|
31366
|
+
return complexScale(p, maxNorm / n);
|
|
31367
|
+
}
|
|
31368
|
+
/**
|
|
31369
|
+
* Möbius transform that translates `a` to the origin: `(z - a) / (1 - conj(a) * z)`.
|
|
31370
|
+
* A hyperbolic isometry — preserves all pairwise Poincaré distances.
|
|
31371
|
+
*/
|
|
31372
|
+
function mobiusToOrigin(z, a) {
|
|
31373
|
+
return clampToDisk(complexDiv(complexSub(z, a), complexSub({
|
|
31374
|
+
x: 1,
|
|
31375
|
+
y: 0
|
|
31376
|
+
}, complexMul(complexConjugate(a), z))));
|
|
31377
|
+
}
|
|
31378
|
+
/**
|
|
31379
|
+
* Inverse of {@link mobiusToOrigin}: translates the origin back to `a`, i.e.
|
|
31380
|
+
* `(z + a) / (1 + conj(a) * z)`.
|
|
31381
|
+
*/
|
|
31382
|
+
function mobiusFromOrigin(z, a) {
|
|
31383
|
+
return clampToDisk(complexDiv(complexAdd(z, a), complexAdd({
|
|
31384
|
+
x: 1,
|
|
31385
|
+
y: 0
|
|
31386
|
+
}, complexMul(complexConjugate(a), z))));
|
|
31387
|
+
}
|
|
31388
|
+
/**
|
|
31389
|
+
* Möbius addition (gyrogroup composition) of two disk-translations: the
|
|
31390
|
+
* canonical-space point reached by translating the origin to `a`, then
|
|
31391
|
+
* translating that local frame's origin by `b`.
|
|
31392
|
+
*/
|
|
31393
|
+
function mobiusAdd(a, b) {
|
|
31394
|
+
return mobiusFromOrigin(b, a);
|
|
31395
|
+
}
|
|
31396
|
+
/** Euclidean Poincaré-disk radius corresponding to hyperbolic distance `d` from the origin. */
|
|
31397
|
+
function euclideanRadiusForHyperbolicDistance(d) {
|
|
31398
|
+
return Math.tanh(d / 2);
|
|
31399
|
+
}
|
|
31400
|
+
/** Hyperbolic distance from the origin corresponding to Euclidean disk radius `r`. */
|
|
31401
|
+
function hyperbolicDistanceForEuclideanRadius(r) {
|
|
31402
|
+
return 2 * Math.atanh(Math.min(Math.max(r, 0), .999999));
|
|
31403
|
+
}
|
|
31404
|
+
/** Hyperbolic (Poincaré) distance between two points in the unit disk. */
|
|
31405
|
+
function poincareDistance(p, q) {
|
|
31406
|
+
return hyperbolicDistanceForEuclideanRadius(norm(mobiusToOrigin(q, p)));
|
|
31407
|
+
}
|
|
31408
|
+
/**
|
|
31409
|
+
* Point at parameter `t` (0–1) along the hyperbolic geodesic from `a` to `b`.
|
|
31410
|
+
* `t=0` yields `a`, `t=1` yields `b`; intermediate values move at constant
|
|
31411
|
+
* hyperbolic speed along the geodesic (not the Euclidean straight line).
|
|
31412
|
+
*/
|
|
31413
|
+
function geodesicInterpolate(a, b, t) {
|
|
31414
|
+
if (t <= 0) return a;
|
|
31415
|
+
if (t >= 1) return b;
|
|
31416
|
+
const bLocal = mobiusToOrigin(b, a);
|
|
31417
|
+
const localNorm = norm(bLocal);
|
|
31418
|
+
if (localNorm < EPSILON) return a;
|
|
31419
|
+
return mobiusFromOrigin(complexScale(complexScale(bLocal, 1 / localNorm), euclideanRadiusForHyperbolicDistance(hyperbolicDistanceForEuclideanRadius(localNorm) * t)), a);
|
|
31420
|
+
}
|
|
31421
|
+
//#endregion
|
|
31422
|
+
//#region src/hyperbolic-tree/layout.ts
|
|
31202
31423
|
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
31203
|
-
function
|
|
31424
|
+
function findHyperbolicNode(root, key) {
|
|
31204
31425
|
if (!root) return void 0;
|
|
31205
31426
|
if (root.item.key === key) return root;
|
|
31206
31427
|
for (const child of root.children ?? []) {
|
|
31207
|
-
const hit =
|
|
31428
|
+
const hit = findHyperbolicNode(child, key);
|
|
31208
31429
|
if (hit) return hit;
|
|
31209
31430
|
}
|
|
31210
31431
|
}
|
|
@@ -31212,92 +31433,289 @@ function findIcicleNode(root, key) {
|
|
|
31212
31433
|
* Chain of nodes from `root` down to (and including) the node with `key`.
|
|
31213
31434
|
* Returns `undefined` when the key is absent. The first entry is always `root`.
|
|
31214
31435
|
*/
|
|
31215
|
-
function
|
|
31436
|
+
function findHyperbolicPath(root, key) {
|
|
31216
31437
|
if (!root) return void 0;
|
|
31217
31438
|
if (root.item.key === key) return [root];
|
|
31218
31439
|
for (const child of root.children ?? []) {
|
|
31219
|
-
const hit =
|
|
31440
|
+
const hit = findHyperbolicPath(child, key);
|
|
31220
31441
|
if (hit) return [root, ...hit];
|
|
31221
31442
|
}
|
|
31222
31443
|
}
|
|
31223
31444
|
/**
|
|
31224
|
-
*
|
|
31445
|
+
* Number of nodes in `node`'s subtree, counting `node` itself, up to
|
|
31446
|
+
* `remainingDepth` further levels (matching the canonical-position recursion's
|
|
31447
|
+
* own `maxDepth` cutoff, so weighting stays consistent with what's actually
|
|
31448
|
+
* laid out).
|
|
31449
|
+
*/
|
|
31450
|
+
function subtreeSize(node, remainingDepth) {
|
|
31451
|
+
if (remainingDepth <= 0) return 1;
|
|
31452
|
+
const children = node.children ?? [];
|
|
31453
|
+
if (children.length === 0) return 1;
|
|
31454
|
+
let total = 1;
|
|
31455
|
+
for (const child of children) total += subtreeSize(child, remainingDepth - 1);
|
|
31456
|
+
return total;
|
|
31457
|
+
}
|
|
31458
|
+
/**
|
|
31459
|
+
* Angle (as seen from `localOrigin`, i.e. after translating `localOrigin` to
|
|
31460
|
+
* the disk's center) of the disk-boundary point that currently sits at angle
|
|
31461
|
+
* `theta` from the true origin. Möbius translation is conformal but not
|
|
31462
|
+
* angle-preserving in the naive sense — a wedge boundary doesn't just shift
|
|
31463
|
+
* by a constant offset when reparented to a new local frame, its exact
|
|
31464
|
+
* transformed angle depends on how far off-center `localOrigin` is. Used to
|
|
31465
|
+
* carry a node's *own* angular wedge (inherited from its parent) into the
|
|
31466
|
+
* local frame of each of its children, rather than approximating it as
|
|
31467
|
+
* "the opposite direction ± half the width" — the same technique used by
|
|
31468
|
+
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
31469
|
+
* `wedgeTranslate`) to keep the recursive layout accurate at any depth/width.
|
|
31470
|
+
*/
|
|
31471
|
+
function boundaryAngleInLocalFrame(theta, localOrigin) {
|
|
31472
|
+
const transformed = mobiusToOrigin({
|
|
31473
|
+
x: Math.cos(theta),
|
|
31474
|
+
y: Math.sin(theta)
|
|
31475
|
+
}, localOrigin);
|
|
31476
|
+
return Math.atan2(transformed.y, transformed.x);
|
|
31477
|
+
}
|
|
31478
|
+
/** Deterministic pseudo-random value in `[-1, 1]` derived from `key`, stable across renders. */
|
|
31479
|
+
function stableUnitJitter(key) {
|
|
31480
|
+
let hash = 0;
|
|
31481
|
+
for (let i = 0; i < key.length; i++) hash = Math.imul(hash, 31) + key.charCodeAt(i) | 0;
|
|
31482
|
+
return (hash >>> 0) % 1e3 / 1e3 * 2 - 1;
|
|
31483
|
+
}
|
|
31484
|
+
/**
|
|
31485
|
+
* A single unbranching child (no siblings to fan against) would otherwise sit
|
|
31486
|
+
* at the exact bisector of its inherited wedge — i.e. dead-center, continuing
|
|
31487
|
+
* in a perfectly straight line from its own parent's edge. For a chain of
|
|
31488
|
+
* such nodes (a deeply-nested path with no branching), that reads as one
|
|
31489
|
+
* unbroken line cutting across the disk rather than a legible sequence of
|
|
31490
|
+
* edges. Nudging by a small, key-derived (so stable across re-renders) angle
|
|
31491
|
+
* breaks that up — the same purpose served by the small per-node offsets in
|
|
31492
|
+
* reference hyperbolic-tree-browser implementations (e.g. d3-hypertree's
|
|
31493
|
+
* `layoutBergé`, which staggers leaf nodes rather than stacking them exactly
|
|
31494
|
+
* radially).
|
|
31495
|
+
*/
|
|
31496
|
+
const CHAIN_JITTER_RADIANS = .22;
|
|
31497
|
+
/**
|
|
31498
|
+
* Canonical (true-root-anchored) hyperbolic position of every currently-loaded
|
|
31499
|
+
* node, keyed by node key. Positions never depend on the current focus —
|
|
31500
|
+
* refocusing is purely a Möbius re-projection of these fixed coordinates, so
|
|
31501
|
+
* the tree never needs to be re-laid-out when the focus changes.
|
|
31225
31502
|
*
|
|
31226
|
-
*
|
|
31227
|
-
*
|
|
31228
|
-
*
|
|
31229
|
-
*
|
|
31230
|
-
*
|
|
31503
|
+
* Each node holds an angular wedge — inherited from its parent, expressed in
|
|
31504
|
+
* its *own* local frame (root starts with the full `[-π, π]`) — across which
|
|
31505
|
+
* its children are placed at hyperbolic distance `curvature` (default `1`).
|
|
31506
|
+
* That wedge is partitioned among the children, weighted by each child's own
|
|
31507
|
+
* subtree size rather than split evenly, and each child's own share is
|
|
31508
|
+
* carried into its local frame via {@link boundaryAngleInLocalFrame} before
|
|
31509
|
+
* recursing. This is a strict partition (slices never overlap and always sum
|
|
31510
|
+
* to the parent's own wedge), which is what guarantees sibling subtrees never
|
|
31511
|
+
* spatially cross regardless of how deep or bushy they are.
|
|
31231
31512
|
*
|
|
31232
|
-
*
|
|
31233
|
-
*
|
|
31234
|
-
*
|
|
31235
|
-
*
|
|
31513
|
+
* `curvature` scales how far apart (in hyperbolic distance) each generation
|
|
31514
|
+
* sits. Raising it pushes each generation further out, so the Poincaré
|
|
31515
|
+
* metric's own `tanh` compression crowds distant generations toward the
|
|
31516
|
+
* boundary faster — a stronger fisheye, which is why it reads as "zooming
|
|
31517
|
+
* in" on the focus. Lowering it spreads generations out more evenly,
|
|
31518
|
+
* flattening the view toward a Euclidean-feeling layout.
|
|
31236
31519
|
*/
|
|
31237
|
-
function
|
|
31238
|
-
|
|
31239
|
-
|
|
31240
|
-
|
|
31241
|
-
|
|
31242
|
-
|
|
31243
|
-
|
|
31244
|
-
|
|
31245
|
-
|
|
31246
|
-
|
|
31247
|
-
|
|
31248
|
-
|
|
31249
|
-
|
|
31250
|
-
|
|
31251
|
-
|
|
31252
|
-
|
|
31253
|
-
const
|
|
31254
|
-
|
|
31255
|
-
|
|
31256
|
-
|
|
31257
|
-
|
|
31258
|
-
|
|
31259
|
-
|
|
31260
|
-
|
|
31261
|
-
|
|
31262
|
-
|
|
31263
|
-
|
|
31264
|
-
|
|
31265
|
-
|
|
31266
|
-
|
|
31267
|
-
|
|
31520
|
+
function computeCanonicalPositions(root, maxDepth = 24, curvature = 1) {
|
|
31521
|
+
const map = /* @__PURE__ */ new Map();
|
|
31522
|
+
if (!root) return map;
|
|
31523
|
+
const origin = {
|
|
31524
|
+
x: 0,
|
|
31525
|
+
y: 0
|
|
31526
|
+
};
|
|
31527
|
+
map.set(root.item.key, {
|
|
31528
|
+
node: root,
|
|
31529
|
+
depth: 0,
|
|
31530
|
+
parentKey: void 0,
|
|
31531
|
+
position: origin,
|
|
31532
|
+
angularShare: 2 * Math.PI
|
|
31533
|
+
});
|
|
31534
|
+
const place = (node, position, wedgeStart, wedgeEnd, depth) => {
|
|
31535
|
+
if (depth >= maxDepth) return;
|
|
31536
|
+
const children = node.children ?? [];
|
|
31537
|
+
const n = children.length;
|
|
31538
|
+
if (n === 0) return;
|
|
31539
|
+
const r0 = euclideanRadiusForHyperbolicDistance(curvature);
|
|
31540
|
+
const angularWidth = wedgeEnd - wedgeStart;
|
|
31541
|
+
const weights = children.map((child) => subtreeSize(child, maxDepth - depth - 1));
|
|
31542
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
31543
|
+
let cursor = wedgeStart;
|
|
31544
|
+
children.forEach((child, index) => {
|
|
31545
|
+
const share = n === 1 ? angularWidth : angularWidth * weights[index] / totalWeight;
|
|
31546
|
+
const childStart = cursor;
|
|
31547
|
+
const childEnd = cursor + share;
|
|
31548
|
+
cursor = childEnd;
|
|
31549
|
+
const theta = (childStart + childEnd) / 2 + (n === 1 ? stableUnitJitter(child.item.key) * Math.min(CHAIN_JITTER_RADIANS, share * .4) : 0);
|
|
31550
|
+
const local = {
|
|
31551
|
+
x: r0 * Math.cos(theta),
|
|
31552
|
+
y: r0 * Math.sin(theta)
|
|
31553
|
+
};
|
|
31554
|
+
const canonicalPosition = mobiusFromOrigin(local, position);
|
|
31555
|
+
map.set(child.item.key, {
|
|
31556
|
+
node: child,
|
|
31557
|
+
depth: depth + 1,
|
|
31558
|
+
parentKey: node.item.key,
|
|
31559
|
+
position: canonicalPosition,
|
|
31560
|
+
angularShare: share
|
|
31268
31561
|
});
|
|
31269
|
-
|
|
31270
|
-
|
|
31562
|
+
const localStart = boundaryAngleInLocalFrame(childStart, local);
|
|
31563
|
+
let localWidth = boundaryAngleInLocalFrame(childEnd, local) - localStart;
|
|
31564
|
+
if (localWidth <= 0) localWidth += 2 * Math.PI;
|
|
31565
|
+
place(child, canonicalPosition, localStart, localStart + localWidth, depth + 1);
|
|
31271
31566
|
});
|
|
31272
31567
|
};
|
|
31273
|
-
|
|
31274
|
-
return
|
|
31568
|
+
place(root, origin, -Math.PI, Math.PI, 0);
|
|
31569
|
+
return map;
|
|
31570
|
+
}
|
|
31571
|
+
/** Nodes within `maxRadius` tree-graph steps of `focusedKey`, mapped to their step distance. */
|
|
31572
|
+
function ringDistances(canonical, focusedKey, maxRadius) {
|
|
31573
|
+
const ring = /* @__PURE__ */ new Map();
|
|
31574
|
+
if (!canonical.has(focusedKey)) return ring;
|
|
31575
|
+
ring.set(focusedKey, 0);
|
|
31576
|
+
let frontier = [focusedKey];
|
|
31577
|
+
for (let distance = 1; distance <= maxRadius && frontier.length > 0; distance++) {
|
|
31578
|
+
const next = [];
|
|
31579
|
+
for (const key of frontier) {
|
|
31580
|
+
const entry = canonical.get(key);
|
|
31581
|
+
if (!entry) continue;
|
|
31582
|
+
const neighbours = [];
|
|
31583
|
+
if (entry.parentKey !== void 0) neighbours.push(entry.parentKey);
|
|
31584
|
+
for (const child of entry.node.children ?? []) neighbours.push(child.item.key);
|
|
31585
|
+
for (const neighbour of neighbours) if (!ring.has(neighbour) && canonical.has(neighbour)) {
|
|
31586
|
+
ring.set(neighbour, distance);
|
|
31587
|
+
next.push(neighbour);
|
|
31588
|
+
}
|
|
31589
|
+
}
|
|
31590
|
+
frontier = next;
|
|
31591
|
+
}
|
|
31592
|
+
return ring;
|
|
31593
|
+
}
|
|
31594
|
+
/**
|
|
31595
|
+
* Compute the visible nodes/edges of a hyperbolic tree view: every
|
|
31596
|
+
* currently-loaded node within `maxRadius` tree-graph steps of the focused
|
|
31597
|
+
* node, projected onto the Poincaré disk with the focus at its center.
|
|
31598
|
+
*
|
|
31599
|
+
* @param root True tree root. `undefined` yields an empty layout.
|
|
31600
|
+
* @param options `focusedKey` (default the true root; unknown keys fall back
|
|
31601
|
+
* to the true root), `maxRadius` (default unlimited), `maxDepth` (safety
|
|
31602
|
+
* cap on canonical-position recursion depth, default 24), `centerOverride`
|
|
31603
|
+
* (project around this canonical-space point instead of the focus node's
|
|
31604
|
+
* own position — used to animate smoothly between foci).
|
|
31605
|
+
*/
|
|
31606
|
+
function computeHyperbolicLayout(root, options = {}) {
|
|
31607
|
+
if (!root) return {
|
|
31608
|
+
nodes: [],
|
|
31609
|
+
edges: []
|
|
31610
|
+
};
|
|
31611
|
+
const { maxRadius = Number.POSITIVE_INFINITY, maxDepth = 24, curvature = 1 } = options;
|
|
31612
|
+
const canonical = computeCanonicalPositions(root, maxDepth, curvature);
|
|
31613
|
+
const focusedKey = options.focusedKey !== void 0 && canonical.has(options.focusedKey) ? options.focusedKey : root.item.key;
|
|
31614
|
+
const focusEntry = canonical.get(focusedKey);
|
|
31615
|
+
if (!focusEntry) return {
|
|
31616
|
+
nodes: [],
|
|
31617
|
+
edges: []
|
|
31618
|
+
};
|
|
31619
|
+
const projectionCenter = options.centerOverride ?? focusEntry.position;
|
|
31620
|
+
const ring = ringDistances(canonical, focusedKey, maxRadius);
|
|
31621
|
+
const rawWeights = [...ring.keys()].map((key) => canonical.get(key).node.item.weight).filter((w) => typeof w === `number`);
|
|
31622
|
+
const minWeight = rawWeights.length > 0 ? Math.min(...rawWeights) : void 0;
|
|
31623
|
+
const maxWeight = rawWeights.length > 0 ? Math.max(...rawWeights) : void 0;
|
|
31624
|
+
const normalizeWeight = (weight) => {
|
|
31625
|
+
if (weight === void 0 || minWeight === void 0 || maxWeight === void 0 || maxWeight === minWeight) return .5;
|
|
31626
|
+
return (weight - minWeight) / (maxWeight - minWeight);
|
|
31627
|
+
};
|
|
31628
|
+
const nodes = [...ring.entries()].map(([key, distance]) => {
|
|
31629
|
+
const entry = canonical.get(key);
|
|
31630
|
+
const weight = entry.node.item.weight;
|
|
31631
|
+
return {
|
|
31632
|
+
node: entry.node,
|
|
31633
|
+
key,
|
|
31634
|
+
depth: entry.depth,
|
|
31635
|
+
ring: distance,
|
|
31636
|
+
parentKey: entry.parentKey,
|
|
31637
|
+
position: mobiusToOrigin(entry.position, projectionCenter),
|
|
31638
|
+
angularShare: entry.angularShare,
|
|
31639
|
+
weight,
|
|
31640
|
+
weightNormalized: normalizeWeight(weight)
|
|
31641
|
+
};
|
|
31642
|
+
}).sort((a, b) => a.ring - b.ring);
|
|
31643
|
+
const byKey = new Map(nodes.map((n) => [n.key, n]));
|
|
31644
|
+
const edges = [];
|
|
31645
|
+
for (const n of nodes) {
|
|
31646
|
+
if (n.parentKey === void 0) continue;
|
|
31647
|
+
const parent = byKey.get(n.parentKey);
|
|
31648
|
+
if (!parent) continue;
|
|
31649
|
+
edges.push({
|
|
31650
|
+
fromKey: parent.key,
|
|
31651
|
+
toKey: n.key,
|
|
31652
|
+
from: parent.position,
|
|
31653
|
+
to: n.position,
|
|
31654
|
+
weightNormalized: (parent.weightNormalized + n.weightNormalized) / 2
|
|
31655
|
+
});
|
|
31656
|
+
}
|
|
31657
|
+
return {
|
|
31658
|
+
nodes,
|
|
31659
|
+
edges
|
|
31660
|
+
};
|
|
31275
31661
|
}
|
|
31276
31662
|
//#endregion
|
|
31277
|
-
//#region src/
|
|
31278
|
-
|
|
31663
|
+
//#region src/hyperbolic-tree/hyperbolictree-list.ts
|
|
31664
|
+
/**
|
|
31665
|
+
* The Poincaré disk's math operates in a `-1..1` unit radius, but rendering
|
|
31666
|
+
* an SVG `viewBox` at that literal scale (then letting CSS stretch it to a
|
|
31667
|
+
* few hundred device pixels) is an extreme, unusual upscale factor that has
|
|
31668
|
+
* been observed to trigger a Chromium GPU-rasterization glitch — a `fill:
|
|
31669
|
+
* none` shape painting as solid-filled, matching its computed style exactly
|
|
31670
|
+
* but not what's on screen. Rendering in a "normal"-magnitude SVG coordinate
|
|
31671
|
+
* system (viewBox radius {@link RENDER_SCALE} instead of `1`) avoids it.
|
|
31672
|
+
*/
|
|
31673
|
+
const RENDER_SCALE = 500;
|
|
31674
|
+
/**
|
|
31675
|
+
* Sanitizes a node key into a token safe to use inside a `part` attribute /
|
|
31676
|
+
* `::part()` selector — arbitrary key characters (spaces, quotes, etc.)
|
|
31677
|
+
* would otherwise produce a token CSS can't select without bespoke escaping.
|
|
31678
|
+
*/
|
|
31679
|
+
function partToken(key) {
|
|
31680
|
+
return key.replace(/[^\w-]/g, `-`);
|
|
31681
|
+
}
|
|
31682
|
+
/** Angular distance (radians, 0–π) of a layout node from screen-"up" (−π/2). */
|
|
31683
|
+
function angleDistanceFromUp(n) {
|
|
31684
|
+
const angle = Math.atan2(n.position.y, n.position.x);
|
|
31685
|
+
let diff = Math.abs(angle - -Math.PI / 2);
|
|
31686
|
+
if (diff > Math.PI) diff = 2 * Math.PI - diff;
|
|
31687
|
+
return diff;
|
|
31688
|
+
}
|
|
31689
|
+
let HyperbolicTreeListElement = class HyperbolicTreeListElement extends i {
|
|
31279
31690
|
constructor(..._args) {
|
|
31280
31691
|
super(..._args);
|
|
31281
31692
|
this.exclusivity = `none`;
|
|
31282
31693
|
this.selectionFilter = `all`;
|
|
31283
31694
|
this.interactionMode = `standard`;
|
|
31284
|
-
this.
|
|
31285
|
-
this.
|
|
31286
|
-
this.
|
|
31287
|
-
this.
|
|
31288
|
-
this.
|
|
31289
|
-
this.
|
|
31695
|
+
this.maxRadius = 4;
|
|
31696
|
+
this.nodeRadius = .05;
|
|
31697
|
+
this.minNodeRadius = .015;
|
|
31698
|
+
this.minWeightScale = .6;
|
|
31699
|
+
this.maxWeightScale = 1.6;
|
|
31700
|
+
this.zoom = 1;
|
|
31701
|
+
this.curvature = 1;
|
|
31702
|
+
this.focusAnimationDuration = 400;
|
|
31290
31703
|
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
31291
31704
|
this.disableKeyboardNavigation = false;
|
|
31292
31705
|
this.registry = void 0;
|
|
31293
|
-
this.
|
|
31706
|
+
this._focusKey = void 0;
|
|
31707
|
+
this._animCenter = void 0;
|
|
31708
|
+
this._panOffset = {
|
|
31709
|
+
x: 0,
|
|
31710
|
+
y: 0
|
|
31711
|
+
};
|
|
31294
31712
|
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
31295
|
-
labelSelector: `.
|
|
31713
|
+
labelSelector: `.hyperbolictree-label`,
|
|
31296
31714
|
highlightKey: `incr-search-highlight`
|
|
31297
31715
|
}) });
|
|
31298
31716
|
this.tickled = new TickledController(this, {
|
|
31299
|
-
getItems: () => this.#
|
|
31300
|
-
id:
|
|
31717
|
+
getItems: () => this.#layout().nodes.map((n) => ({
|
|
31718
|
+
id: n.key,
|
|
31301
31719
|
enabled: true
|
|
31302
31720
|
})),
|
|
31303
31721
|
onStateChange: () => {
|
|
@@ -31305,12 +31723,12 @@ let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
|
31305
31723
|
},
|
|
31306
31724
|
onTickle: (id) => {
|
|
31307
31725
|
if (id === void 0) return;
|
|
31308
|
-
const
|
|
31309
|
-
if (!
|
|
31726
|
+
const n = this.#layout().nodes.find((n) => n.key === id);
|
|
31727
|
+
if (!n) return;
|
|
31310
31728
|
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
31311
31729
|
detail: {
|
|
31312
|
-
node:
|
|
31313
|
-
depth:
|
|
31730
|
+
node: n.node,
|
|
31731
|
+
depth: n.depth
|
|
31314
31732
|
},
|
|
31315
31733
|
bubbles: true,
|
|
31316
31734
|
composed: true
|
|
@@ -31329,29 +31747,40 @@ let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
|
31329
31747
|
get selectedNodes() {
|
|
31330
31748
|
return this._selectedNodes;
|
|
31331
31749
|
}
|
|
31332
|
-
/**
|
|
31750
|
+
/** The node currently centered on the disk. Pure view-state — `root` is never mutated by focus changes. */
|
|
31333
31751
|
get focusedNode() {
|
|
31334
|
-
if (this.
|
|
31335
|
-
return
|
|
31752
|
+
if (this._focusKey === void 0) return this.#effectiveRoot;
|
|
31753
|
+
return findHyperbolicNode(this.#effectiveRoot, this._focusKey) ?? this.#effectiveRoot;
|
|
31336
31754
|
}
|
|
31337
31755
|
#modelUnsubscribe;
|
|
31338
31756
|
#pendingKeys;
|
|
31339
31757
|
#aborters;
|
|
31340
|
-
#anchorIndex;
|
|
31341
31758
|
#ownedRegistry;
|
|
31342
31759
|
#unregisterSearchCommands;
|
|
31343
|
-
#
|
|
31760
|
+
#animFrame;
|
|
31761
|
+
#panStart;
|
|
31344
31762
|
get #effectiveRoot() {
|
|
31345
31763
|
return this.model?.root ?? this.root;
|
|
31346
31764
|
}
|
|
31347
|
-
#
|
|
31348
|
-
return
|
|
31349
|
-
|
|
31350
|
-
|
|
31351
|
-
|
|
31352
|
-
|
|
31765
|
+
#layout() {
|
|
31766
|
+
return computeHyperbolicLayout(this.#effectiveRoot, {
|
|
31767
|
+
focusedKey: this._focusKey,
|
|
31768
|
+
maxRadius: this.maxRadius,
|
|
31769
|
+
curvature: this.curvature,
|
|
31770
|
+
centerOverride: this._panOffset.x === 0 && this._panOffset.y === 0 && this._animCenter === void 0 ? void 0 : this.#effectiveCenter()
|
|
31353
31771
|
});
|
|
31354
31772
|
}
|
|
31773
|
+
#effectiveCenter() {
|
|
31774
|
+
const canonical = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature);
|
|
31775
|
+
const focusKey = this._focusKey ?? this.#effectiveRoot?.item.key;
|
|
31776
|
+
const base = this._animCenter ?? (focusKey === void 0 ? void 0 : canonical.get(focusKey)?.position);
|
|
31777
|
+
if (base === void 0) return void 0;
|
|
31778
|
+
if (this._panOffset.x === 0 && this._panOffset.y === 0) return base;
|
|
31779
|
+
return clampToDisk({
|
|
31780
|
+
x: base.x + this._panOffset.x,
|
|
31781
|
+
y: base.y + this._panOffset.y
|
|
31782
|
+
}, .95);
|
|
31783
|
+
}
|
|
31355
31784
|
connectedCallback() {
|
|
31356
31785
|
super.connectedCallback();
|
|
31357
31786
|
this.setAttribute(`data-input-mode`, `mouse`);
|
|
@@ -31370,10 +31799,6 @@ let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
|
31370
31799
|
});
|
|
31371
31800
|
this.registry = registry;
|
|
31372
31801
|
this.#syncSearchCommands();
|
|
31373
|
-
if (typeof ResizeObserver !== `undefined`) {
|
|
31374
|
-
this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
|
|
31375
|
-
this.#resizeObserver.observe(this);
|
|
31376
|
-
}
|
|
31377
31802
|
}
|
|
31378
31803
|
disconnectedCallback() {
|
|
31379
31804
|
super.disconnectedCallback();
|
|
@@ -31382,8 +31807,10 @@ let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
|
31382
31807
|
for (const aborter of this.#aborters.values()) aborter.abort();
|
|
31383
31808
|
this.#aborters.clear();
|
|
31384
31809
|
this.#pendingKeys.clear();
|
|
31385
|
-
this.#
|
|
31386
|
-
|
|
31810
|
+
if (this.#animFrame !== void 0) {
|
|
31811
|
+
cancelAnimationFrame(this.#animFrame);
|
|
31812
|
+
this.#animFrame = void 0;
|
|
31813
|
+
}
|
|
31387
31814
|
if (this.#ownedRegistry) {
|
|
31388
31815
|
this.#ownedRegistry.unregister(`select-all`);
|
|
31389
31816
|
this.#ownedRegistry.unregister(`deselect-all`);
|
|
@@ -31418,74 +31845,1010 @@ let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
|
31418
31845
|
this.#resetViewState();
|
|
31419
31846
|
}
|
|
31420
31847
|
}
|
|
31421
|
-
if (changedProps.has(`interactionMode`))
|
|
31422
|
-
this.#anchorIndex = void 0;
|
|
31423
|
-
this.clearSelection();
|
|
31424
|
-
}
|
|
31848
|
+
if (changedProps.has(`interactionMode`)) this.clearSelection();
|
|
31425
31849
|
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
31426
|
-
if (this.
|
|
31850
|
+
if (this._focusKey !== void 0 && !findHyperbolicNode(this.#effectiveRoot, this._focusKey)) {
|
|
31851
|
+
this._focusKey = void 0;
|
|
31852
|
+
this._animCenter = void 0;
|
|
31853
|
+
}
|
|
31427
31854
|
this.#ensureVisibleChildren();
|
|
31428
|
-
this.#syncTooltip();
|
|
31429
|
-
}
|
|
31430
|
-
/** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
|
|
31431
|
-
#hostExtent() {
|
|
31432
|
-
return (this.orientation === `vertical` ? this.clientHeight : this.clientWidth) || 0;
|
|
31433
|
-
}
|
|
31434
|
-
/**
|
|
31435
|
-
* Whether `rect`'s label should be rotated or hidden for lack of space, per
|
|
31436
|
-
* {@link minLabelWidth} / {@link minVerticalLabelWidth}. `orientation="vertical"`
|
|
31437
|
-
* rects never rotate — hides outright at `minLabelWidth` instead, same as before.
|
|
31438
|
-
*/
|
|
31439
|
-
#labelMode(rect) {
|
|
31440
|
-
const hostExtent = this.#hostExtent();
|
|
31441
|
-
if (hostExtent === 0) return {
|
|
31442
|
-
hidden: false,
|
|
31443
|
-
rotated: false
|
|
31444
|
-
};
|
|
31445
|
-
const vertical = this.orientation === `vertical`;
|
|
31446
|
-
const pixelWidth = rect.width * hostExtent;
|
|
31447
|
-
if (vertical) return {
|
|
31448
|
-
hidden: pixelWidth < this.minLabelWidth,
|
|
31449
|
-
rotated: false
|
|
31450
|
-
};
|
|
31451
|
-
if (pixelWidth < this.minVerticalLabelWidth) return {
|
|
31452
|
-
hidden: true,
|
|
31453
|
-
rotated: false
|
|
31454
|
-
};
|
|
31455
|
-
return {
|
|
31456
|
-
hidden: false,
|
|
31457
|
-
rotated: pixelWidth < this.minLabelWidth
|
|
31458
|
-
};
|
|
31459
|
-
}
|
|
31460
|
-
/** Anchor the shared tooltip popover to the tickled rect, if its label needs one. */
|
|
31461
|
-
#syncTooltip() {
|
|
31462
|
-
const popover = this._tooltipEl;
|
|
31463
|
-
if (!popover?.popover) return;
|
|
31464
|
-
const key = this.tickled.tickledId;
|
|
31465
|
-
const rect = key === void 0 ? void 0 : this.#rects().find((r) => r.node.item.key === key);
|
|
31466
|
-
const mode = rect ? this.#labelMode(rect) : void 0;
|
|
31467
|
-
const isOpen = popover.matches(`:popover-open`);
|
|
31468
|
-
if (rect && mode && (mode.hidden || mode.rotated)) {
|
|
31469
|
-
popover.textContent = rect.node.item.label;
|
|
31470
|
-
if (!isOpen) try {
|
|
31471
|
-
popover.showPopover();
|
|
31472
|
-
} catch {}
|
|
31473
|
-
} else if (isOpen) try {
|
|
31474
|
-
popover.hidePopover();
|
|
31475
|
-
} catch {}
|
|
31476
31855
|
}
|
|
31477
31856
|
#resetViewState() {
|
|
31478
31857
|
this.#clearSelectionInternal();
|
|
31479
31858
|
this.tickled.clearTickled();
|
|
31480
|
-
this.
|
|
31481
|
-
this
|
|
31859
|
+
this._focusKey = void 0;
|
|
31860
|
+
this._animCenter = void 0;
|
|
31861
|
+
this._panOffset = {
|
|
31862
|
+
x: 0,
|
|
31863
|
+
y: 0
|
|
31864
|
+
};
|
|
31865
|
+
if (this.#animFrame !== void 0) {
|
|
31866
|
+
cancelAnimationFrame(this.#animFrame);
|
|
31867
|
+
this.#animFrame = void 0;
|
|
31868
|
+
}
|
|
31482
31869
|
}
|
|
31483
31870
|
#ensureVisibleChildren() {
|
|
31484
31871
|
if (!this.model && !this.loadChildren) return;
|
|
31485
|
-
for (const
|
|
31486
|
-
const { node } =
|
|
31872
|
+
for (const n of this.#layout().nodes) {
|
|
31873
|
+
const { node } = n;
|
|
31487
31874
|
if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
|
|
31488
|
-
this.#loadChildren(node,
|
|
31875
|
+
this.#loadChildren(node, n.depth);
|
|
31876
|
+
}
|
|
31877
|
+
}
|
|
31878
|
+
async #loadChildren(node, depth) {
|
|
31879
|
+
const key = node.item.key;
|
|
31880
|
+
this.#pendingKeys.add(key);
|
|
31881
|
+
const controller = new AbortController();
|
|
31882
|
+
this.#aborters.set(key, controller);
|
|
31883
|
+
this.requestUpdate();
|
|
31884
|
+
try {
|
|
31885
|
+
let children;
|
|
31886
|
+
if (this.model) children = await this.model.getChildren(node, depth, controller.signal);
|
|
31887
|
+
else children = [...await collectDataResult(this.loadChildren({
|
|
31888
|
+
node,
|
|
31889
|
+
depth
|
|
31890
|
+
}, controller.signal), controller.signal)];
|
|
31891
|
+
if (controller.signal.aborted) return;
|
|
31892
|
+
node.children = children;
|
|
31893
|
+
} catch {
|
|
31894
|
+
if (controller.signal.aborted) return;
|
|
31895
|
+
node.children = [];
|
|
31896
|
+
} finally {
|
|
31897
|
+
this.#pendingKeys.delete(key);
|
|
31898
|
+
this.#aborters.delete(key);
|
|
31899
|
+
if (!controller.signal.aborted) this.requestUpdate();
|
|
31900
|
+
}
|
|
31901
|
+
}
|
|
31902
|
+
#canSelect(node) {
|
|
31903
|
+
if (this.selectionFilter === `none`) return false;
|
|
31904
|
+
if (this.selectionFilter === `leaf`) return node.item.isLeaf === true;
|
|
31905
|
+
if (this.selectionFilter === `branch`) return node.item.isLeaf === false || node.children !== void 0 && node.children.length > 0;
|
|
31906
|
+
return true;
|
|
31907
|
+
}
|
|
31908
|
+
#clearSelectionInternal() {
|
|
31909
|
+
this._selectedNodes.clear();
|
|
31910
|
+
this.selectedNode = void 0;
|
|
31911
|
+
}
|
|
31912
|
+
#dispatchSelect(previous) {
|
|
31913
|
+
this.dispatchEvent(new CustomEvent(`select`, {
|
|
31914
|
+
detail: {
|
|
31915
|
+
selected: new Set(this._selectedNodes),
|
|
31916
|
+
previous
|
|
31917
|
+
},
|
|
31918
|
+
bubbles: true,
|
|
31919
|
+
composed: true
|
|
31920
|
+
}));
|
|
31921
|
+
}
|
|
31922
|
+
select(node) {
|
|
31923
|
+
if (!this.#canSelect(node)) return;
|
|
31924
|
+
const prev = new Set(this._selectedNodes);
|
|
31925
|
+
this._selectedNodes.clear();
|
|
31926
|
+
this._selectedNodes.add(node);
|
|
31927
|
+
this.selectedNode = node;
|
|
31928
|
+
this.#dispatchSelect(prev);
|
|
31929
|
+
this.requestUpdate();
|
|
31930
|
+
}
|
|
31931
|
+
deselect(node) {
|
|
31932
|
+
if (!this._selectedNodes.has(node)) return;
|
|
31933
|
+
const prev = new Set(this._selectedNodes);
|
|
31934
|
+
this._selectedNodes.delete(node);
|
|
31935
|
+
if (this.selectedNode === node) this.selectedNode = [...this._selectedNodes].at(-1);
|
|
31936
|
+
this.#dispatchSelect(prev);
|
|
31937
|
+
this.requestUpdate();
|
|
31938
|
+
}
|
|
31939
|
+
clearSelection() {
|
|
31940
|
+
if (this._selectedNodes.size === 0) return;
|
|
31941
|
+
const prev = new Set(this._selectedNodes);
|
|
31942
|
+
this.#clearSelectionInternal();
|
|
31943
|
+
this.#dispatchSelect(prev);
|
|
31944
|
+
this.requestUpdate();
|
|
31945
|
+
}
|
|
31946
|
+
selectAll() {
|
|
31947
|
+
const prev = new Set(this._selectedNodes);
|
|
31948
|
+
for (const n of this.#layout().nodes) {
|
|
31949
|
+
if (!this.#canSelect(n.node)) continue;
|
|
31950
|
+
this._selectedNodes.add(n.node);
|
|
31951
|
+
}
|
|
31952
|
+
this.selectedNode = [...this._selectedNodes].at(-1);
|
|
31953
|
+
this.#dispatchSelect(prev);
|
|
31954
|
+
this.requestUpdate();
|
|
31955
|
+
}
|
|
31956
|
+
deselectAll() {
|
|
31957
|
+
const prev = new Set(this._selectedNodes);
|
|
31958
|
+
this.#clearSelectionInternal();
|
|
31959
|
+
this.#dispatchSelect(prev);
|
|
31960
|
+
this.requestUpdate();
|
|
31961
|
+
}
|
|
31962
|
+
/** Replace the selection with `nodes` (subject to `selectionFilter`) — search "select" mode. */
|
|
31963
|
+
selectMany(nodes) {
|
|
31964
|
+
const prev = new Set(this._selectedNodes);
|
|
31965
|
+
this._selectedNodes = new Set([...nodes].filter((n) => this.#canSelect(n)));
|
|
31966
|
+
this.selectedNode = [...this._selectedNodes].at(-1);
|
|
31967
|
+
this.#dispatchSelect(prev);
|
|
31968
|
+
this.requestUpdate();
|
|
31969
|
+
}
|
|
31970
|
+
/**
|
|
31971
|
+
* Re-center the disk on `node` (an animated Möbius transform of the fixed
|
|
31972
|
+
* layout — `root` is never mutated). Fires `focus`, plus the semantic
|
|
31973
|
+
* `expand`/`collapse` event when the new focus is a descendant/ancestor of
|
|
31974
|
+
* the previous one, for interop with generic `TreeComponent` listeners.
|
|
31975
|
+
*/
|
|
31976
|
+
focusOn(node) {
|
|
31977
|
+
const targetKey = node.item.key;
|
|
31978
|
+
const currentFocusKey = this._focusKey ?? this.#effectiveRoot?.item.key;
|
|
31979
|
+
if (currentFocusKey === targetKey) return;
|
|
31980
|
+
const previousNode = currentFocusKey === void 0 ? void 0 : findHyperbolicNode(this.#effectiveRoot, currentFocusKey);
|
|
31981
|
+
const canonical = computeCanonicalPositions(this.#effectiveRoot, 24, this.curvature);
|
|
31982
|
+
const fromPosition = (currentFocusKey === void 0 ? void 0 : canonical.get(currentFocusKey)?.position) ?? {
|
|
31983
|
+
x: 0,
|
|
31984
|
+
y: 0
|
|
31985
|
+
};
|
|
31986
|
+
const toPosition = canonical.get(targetKey)?.position ?? {
|
|
31987
|
+
x: 0,
|
|
31988
|
+
y: 0
|
|
31989
|
+
};
|
|
31990
|
+
this._focusKey = targetKey;
|
|
31991
|
+
this._panOffset = {
|
|
31992
|
+
x: 0,
|
|
31993
|
+
y: 0
|
|
31994
|
+
};
|
|
31995
|
+
this.#animateTo(fromPosition, toPosition);
|
|
31996
|
+
this.dispatchEvent(new CustomEvent(`focus`, {
|
|
31997
|
+
detail: {
|
|
31998
|
+
node,
|
|
31999
|
+
previous: previousNode
|
|
32000
|
+
},
|
|
32001
|
+
bubbles: true,
|
|
32002
|
+
composed: true
|
|
32003
|
+
}));
|
|
32004
|
+
if (previousNode) {
|
|
32005
|
+
if (findHyperbolicNode(previousNode, targetKey)) {
|
|
32006
|
+
const depth = (findHyperbolicPath(this.#effectiveRoot, targetKey)?.length ?? 1) - 1;
|
|
32007
|
+
this.dispatchEvent(new CustomEvent(`expand`, {
|
|
32008
|
+
detail: {
|
|
32009
|
+
node,
|
|
32010
|
+
depth
|
|
32011
|
+
},
|
|
32012
|
+
bubbles: true,
|
|
32013
|
+
composed: true
|
|
32014
|
+
}));
|
|
32015
|
+
} else if (findHyperbolicNode(node, previousNode.item.key)) {
|
|
32016
|
+
const depth = (findHyperbolicPath(this.#effectiveRoot, previousNode.item.key)?.length ?? 1) - 1;
|
|
32017
|
+
this.dispatchEvent(new CustomEvent(`collapse`, {
|
|
32018
|
+
detail: {
|
|
32019
|
+
node: previousNode,
|
|
32020
|
+
depth
|
|
32021
|
+
},
|
|
32022
|
+
bubbles: true,
|
|
32023
|
+
composed: true
|
|
32024
|
+
}));
|
|
32025
|
+
}
|
|
32026
|
+
}
|
|
32027
|
+
this.requestUpdate();
|
|
32028
|
+
}
|
|
32029
|
+
#prefersReducedMotion() {
|
|
32030
|
+
return typeof matchMedia === `function` && matchMedia(`(prefers-reduced-motion: reduce)`).matches;
|
|
32031
|
+
}
|
|
32032
|
+
#animateTo(from, to) {
|
|
32033
|
+
if (this.#animFrame !== void 0) {
|
|
32034
|
+
cancelAnimationFrame(this.#animFrame);
|
|
32035
|
+
this.#animFrame = void 0;
|
|
32036
|
+
}
|
|
32037
|
+
if (this.focusAnimationDuration <= 0 || this.#prefersReducedMotion() || typeof requestAnimationFrame !== `function`) {
|
|
32038
|
+
this._animCenter = void 0;
|
|
32039
|
+
return;
|
|
32040
|
+
}
|
|
32041
|
+
const start = performance.now();
|
|
32042
|
+
const duration = this.focusAnimationDuration;
|
|
32043
|
+
const step = (now) => {
|
|
32044
|
+
const t = Math.min(1, (now - start) / duration);
|
|
32045
|
+
this._animCenter = t >= 1 ? void 0 : geodesicInterpolate(from, to, t);
|
|
32046
|
+
this.requestUpdate();
|
|
32047
|
+
this.#animFrame = t < 1 ? requestAnimationFrame(step) : void 0;
|
|
32048
|
+
};
|
|
32049
|
+
this.#animFrame = requestAnimationFrame(step);
|
|
32050
|
+
}
|
|
32051
|
+
#focusAndMaybeSelect(node) {
|
|
32052
|
+
this.focusOn(node);
|
|
32053
|
+
this.tickled.setTickled(node.item.key);
|
|
32054
|
+
if (this.interactionMode === `implicit`) this.select(node);
|
|
32055
|
+
}
|
|
32056
|
+
/** Focus + select the node at `keys` (path from the root's children). Returns `true` if found. */
|
|
32057
|
+
navigateTo(keys) {
|
|
32058
|
+
const root = this.#effectiveRoot;
|
|
32059
|
+
if (!root?.children) return false;
|
|
32060
|
+
let current = root;
|
|
32061
|
+
for (const key of keys) {
|
|
32062
|
+
const found = current.children?.find((n) => n.item.key === key);
|
|
32063
|
+
if (!found) return false;
|
|
32064
|
+
current = found;
|
|
32065
|
+
}
|
|
32066
|
+
this.focusOn(current);
|
|
32067
|
+
this.select(current);
|
|
32068
|
+
this.tickled.setTickled(current.item.key);
|
|
32069
|
+
this.requestUpdate();
|
|
32070
|
+
return true;
|
|
32071
|
+
}
|
|
32072
|
+
/** Focus + select the first node for which `predicate` returns `true`. Returns `true` if found. */
|
|
32073
|
+
navigateToNode(predicate) {
|
|
32074
|
+
const root = this.#effectiveRoot;
|
|
32075
|
+
if (!root?.children) return false;
|
|
32076
|
+
const keyPath = this.#findKeyPath(root.children, predicate);
|
|
32077
|
+
if (!keyPath) return false;
|
|
32078
|
+
return this.navigateTo(keyPath);
|
|
32079
|
+
}
|
|
32080
|
+
/**
|
|
32081
|
+
* Bring the node with `key` into the visible window and move the keyboard
|
|
32082
|
+
* cursor to it, without changing the selection. Used by incremental-search
|
|
32083
|
+
* "navigate" mode.
|
|
32084
|
+
*/
|
|
32085
|
+
revealKey(key) {
|
|
32086
|
+
const node = findHyperbolicNode(this.#effectiveRoot, key);
|
|
32087
|
+
if (!node) return false;
|
|
32088
|
+
if (!this.#layout().nodes.some((n) => n.key === key)) this.focusOn(node);
|
|
32089
|
+
this.tickled.setTickled(key);
|
|
32090
|
+
this.requestUpdate();
|
|
32091
|
+
return true;
|
|
32092
|
+
}
|
|
32093
|
+
#findKeyPath(nodes, predicate, prefix = []) {
|
|
32094
|
+
for (const node of nodes) {
|
|
32095
|
+
const path = [...prefix, node.item.key];
|
|
32096
|
+
if (predicate(node)) return path;
|
|
32097
|
+
if (node.children?.length) {
|
|
32098
|
+
const hit = this.#findKeyPath(node.children, predicate, path);
|
|
32099
|
+
if (hit) return hit;
|
|
32100
|
+
}
|
|
32101
|
+
}
|
|
32102
|
+
}
|
|
32103
|
+
/**
|
|
32104
|
+
* All nodes currently laid out on the disk (within `maxRadius` of the
|
|
32105
|
+
* focus), in render (ring-major) order. Unlike `ixfx-icicletree-list`, the
|
|
32106
|
+
* focused node itself is included — it is literally rendered, at the
|
|
32107
|
+
* center.
|
|
32108
|
+
*/
|
|
32109
|
+
getNodes() {
|
|
32110
|
+
return this.#layout().nodes.map((n) => ({
|
|
32111
|
+
node: n.node,
|
|
32112
|
+
depth: n.depth
|
|
32113
|
+
}));
|
|
32114
|
+
}
|
|
32115
|
+
#matchesFilter(node, depth) {
|
|
32116
|
+
if (!this.filterPredicate) return true;
|
|
32117
|
+
if (this.filterPredicate(node, depth)) return true;
|
|
32118
|
+
return node.children?.some((child) => this.#matchesFilter(child, depth + 1)) ?? false;
|
|
32119
|
+
}
|
|
32120
|
+
#handleItemClick(n, event) {
|
|
32121
|
+
this.dispatchEvent(new CustomEvent(`item-click`, {
|
|
32122
|
+
detail: {
|
|
32123
|
+
node: n.node,
|
|
32124
|
+
depth: n.depth
|
|
32125
|
+
},
|
|
32126
|
+
bubbles: true,
|
|
32127
|
+
composed: true
|
|
32128
|
+
}));
|
|
32129
|
+
if (this.interactionMode === `standard` && (event.ctrlKey || event.metaKey)) {
|
|
32130
|
+
if (this._selectedNodes.has(n.node)) this.deselect(n.node);
|
|
32131
|
+
else if (this.#canSelect(n.node)) {
|
|
32132
|
+
const prev = new Set(this._selectedNodes);
|
|
32133
|
+
this._selectedNodes.add(n.node);
|
|
32134
|
+
this.selectedNode = n.node;
|
|
32135
|
+
this.#dispatchSelect(prev);
|
|
32136
|
+
this.requestUpdate();
|
|
32137
|
+
}
|
|
32138
|
+
} else if (this.interactionMode !== `manual`) this.select(n.node);
|
|
32139
|
+
this.tickled.setTickled(n.key);
|
|
32140
|
+
this.focusOn(n.node);
|
|
32141
|
+
}
|
|
32142
|
+
#handleItemDblClick(n) {
|
|
32143
|
+
this.dispatchEvent(new CustomEvent(`item-dblclick`, {
|
|
32144
|
+
detail: {
|
|
32145
|
+
node: n.node,
|
|
32146
|
+
depth: n.depth
|
|
32147
|
+
},
|
|
32148
|
+
bubbles: true,
|
|
32149
|
+
composed: true
|
|
32150
|
+
}));
|
|
32151
|
+
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
32152
|
+
detail: {
|
|
32153
|
+
node: n.node,
|
|
32154
|
+
depth: n.depth
|
|
32155
|
+
},
|
|
32156
|
+
bubbles: true,
|
|
32157
|
+
composed: true
|
|
32158
|
+
}));
|
|
32159
|
+
}
|
|
32160
|
+
#handleKeyDown(event) {
|
|
32161
|
+
if (this.disableKeyboardNavigation) return;
|
|
32162
|
+
const root = this.#effectiveRoot;
|
|
32163
|
+
const { nodes } = this.#layout();
|
|
32164
|
+
if (!root || nodes.length === 0) return;
|
|
32165
|
+
const focusNode = findHyperbolicNode(root, this._focusKey ?? root.item.key);
|
|
32166
|
+
if (!focusNode) return;
|
|
32167
|
+
switch (event.key) {
|
|
32168
|
+
case `ArrowUp`: {
|
|
32169
|
+
event.preventDefault();
|
|
32170
|
+
const children = nodes.filter((n) => n.parentKey === focusNode.item.key);
|
|
32171
|
+
if (children.length === 0) return;
|
|
32172
|
+
const best = children.reduce((a, b) => angleDistanceFromUp(a) < angleDistanceFromUp(b) ? a : b);
|
|
32173
|
+
this.#focusAndMaybeSelect(best.node);
|
|
32174
|
+
return;
|
|
32175
|
+
}
|
|
32176
|
+
case `ArrowDown`: {
|
|
32177
|
+
event.preventDefault();
|
|
32178
|
+
const path = findHyperbolicPath(root, focusNode.item.key);
|
|
32179
|
+
const parent = path && path.length >= 2 ? path[path.length - 2] : void 0;
|
|
32180
|
+
if (parent) this.#focusAndMaybeSelect(parent);
|
|
32181
|
+
return;
|
|
32182
|
+
}
|
|
32183
|
+
case `ArrowLeft`:
|
|
32184
|
+
case `ArrowRight`: {
|
|
32185
|
+
event.preventDefault();
|
|
32186
|
+
const path = findHyperbolicPath(root, focusNode.item.key);
|
|
32187
|
+
const siblings = (path && path.length >= 2 ? path[path.length - 2] : void 0)?.children ?? (focusNode === root ? [focusNode] : []);
|
|
32188
|
+
const index = siblings.findIndex((n) => n.item.key === focusNode.item.key);
|
|
32189
|
+
if (index === -1 || siblings.length <= 1) return;
|
|
32190
|
+
const next = siblings[(index + (event.key === `ArrowRight` ? 1 : -1) + siblings.length) % siblings.length];
|
|
32191
|
+
this.#focusAndMaybeSelect(next);
|
|
32192
|
+
return;
|
|
32193
|
+
}
|
|
32194
|
+
case `Enter`: {
|
|
32195
|
+
event.preventDefault();
|
|
32196
|
+
if (this.interactionMode !== `manual`) this.select(focusNode);
|
|
32197
|
+
const depth = (findHyperbolicPath(root, focusNode.item.key)?.length ?? 1) - 1;
|
|
32198
|
+
this.dispatchEvent(new CustomEvent(`activate`, {
|
|
32199
|
+
detail: {
|
|
32200
|
+
node: focusNode,
|
|
32201
|
+
depth
|
|
32202
|
+
},
|
|
32203
|
+
bubbles: true,
|
|
32204
|
+
composed: true
|
|
32205
|
+
}));
|
|
32206
|
+
return;
|
|
32207
|
+
}
|
|
32208
|
+
case `Escape`: if (this._focusKey !== void 0 && this._focusKey !== root.item.key) {
|
|
32209
|
+
event.preventDefault();
|
|
32210
|
+
this.focusOn(root);
|
|
32211
|
+
}
|
|
32212
|
+
}
|
|
32213
|
+
}
|
|
32214
|
+
#isBackgroundTarget(event) {
|
|
32215
|
+
const target = event.target;
|
|
32216
|
+
return target === event.currentTarget || target?.classList.contains(`disk-boundary`) === true;
|
|
32217
|
+
}
|
|
32218
|
+
#handleBackgroundPointerDown(event) {
|
|
32219
|
+
if (!this.#isBackgroundTarget(event)) return;
|
|
32220
|
+
event.currentTarget.setPointerCapture(event.pointerId);
|
|
32221
|
+
this.#panStart = {
|
|
32222
|
+
pointerId: event.pointerId,
|
|
32223
|
+
startClient: {
|
|
32224
|
+
x: event.clientX,
|
|
32225
|
+
y: event.clientY
|
|
32226
|
+
},
|
|
32227
|
+
baseOffset: this._panOffset
|
|
32228
|
+
};
|
|
32229
|
+
}
|
|
32230
|
+
#handleBackgroundPointerMove(event) {
|
|
32231
|
+
if (!this.#panStart || event.pointerId !== this.#panStart.pointerId) return;
|
|
32232
|
+
const rect = event.currentTarget.getBoundingClientRect();
|
|
32233
|
+
const scale = 2.1 / (Math.min(rect.width, rect.height) || 1);
|
|
32234
|
+
const dx = (event.clientX - this.#panStart.startClient.x) * scale;
|
|
32235
|
+
const dy = (event.clientY - this.#panStart.startClient.y) * scale;
|
|
32236
|
+
this._panOffset = clampToDisk({
|
|
32237
|
+
x: this.#panStart.baseOffset.x - dx,
|
|
32238
|
+
y: this.#panStart.baseOffset.y - dy
|
|
32239
|
+
}, .9);
|
|
32240
|
+
this.requestUpdate();
|
|
32241
|
+
}
|
|
32242
|
+
#handleBackgroundPointerUp(event) {
|
|
32243
|
+
if (this.#panStart?.pointerId === event.pointerId) this.#panStart = void 0;
|
|
32244
|
+
}
|
|
32245
|
+
#defaultEdgeRenderer(edge) {
|
|
32246
|
+
const steps = 8;
|
|
32247
|
+
let d = ``;
|
|
32248
|
+
for (let i = 0; i <= steps; i++) {
|
|
32249
|
+
const p = geodesicInterpolate(edge.from, edge.to, i / steps);
|
|
32250
|
+
d += `${i === 0 ? `M` : `L`}${(p.x * RENDER_SCALE).toFixed(2)},${(p.y * RENDER_SCALE).toFixed(2)} `;
|
|
32251
|
+
}
|
|
32252
|
+
return w`<path
|
|
32253
|
+
class="edge"
|
|
32254
|
+
d=${d}
|
|
32255
|
+
part="edge edge-to-${partToken(edge.toKey)} edge-from-${partToken(edge.fromKey)}"
|
|
32256
|
+
data-from=${edge.fromKey}
|
|
32257
|
+
data-to=${edge.toKey}
|
|
32258
|
+
style="--edge-weight: ${edge.weightNormalized}"
|
|
32259
|
+
/>`;
|
|
32260
|
+
}
|
|
32261
|
+
#renderEdge(edge, byKey) {
|
|
32262
|
+
if (this.edgeRenderer) {
|
|
32263
|
+
const fromNode = byKey.get(edge.fromKey)?.node;
|
|
32264
|
+
const toNode = byKey.get(edge.toKey)?.node;
|
|
32265
|
+
if (fromNode && toNode) return w`<g transform="scale(${RENDER_SCALE})">${this.edgeRenderer({
|
|
32266
|
+
edge,
|
|
32267
|
+
fromNode,
|
|
32268
|
+
toNode
|
|
32269
|
+
})}</g>`;
|
|
32270
|
+
}
|
|
32271
|
+
return this.#defaultEdgeRenderer(edge);
|
|
32272
|
+
}
|
|
32273
|
+
/**
|
|
32274
|
+
* A node's rendered dot radius: shrinks with distance from focus (existing
|
|
32275
|
+
* behaviour), but also caps itself to the angular room {@link
|
|
32276
|
+
* HyperbolicLayoutNode.angularShare} actually gives it at its radius —
|
|
32277
|
+
* without this, a cluster of siblings squeezed into a narrow wedge (a
|
|
32278
|
+
* bushy ancestor elsewhere in the tree, or many siblings at this level)
|
|
32279
|
+
* all render at the same distance-only size and physically overlap into
|
|
32280
|
+
* an unreadable blob, however small {@link minNodeRadius} suppresses their
|
|
32281
|
+
* labels. `angularShare * distance` approximates the Euclidean arc length
|
|
32282
|
+
* available to the node; capping the diameter to a fraction of that keeps
|
|
32283
|
+
* a visible gap between crowded neighbours.
|
|
32284
|
+
*/
|
|
32285
|
+
#nodeRenderRadius(n) {
|
|
32286
|
+
const distance = Math.hypot(n.position.x, n.position.y);
|
|
32287
|
+
const scale = Math.max(.2, 1 - distance);
|
|
32288
|
+
const weightScale = this.minWeightScale + (this.maxWeightScale - this.minWeightScale) * n.weightNormalized;
|
|
32289
|
+
const radius = this.nodeRadius * scale * weightScale;
|
|
32290
|
+
if (distance < 1e-6) return radius;
|
|
32291
|
+
const arcLength = n.angularShare * distance;
|
|
32292
|
+
return Math.min(radius, Math.max(arcLength * .35, .003));
|
|
32293
|
+
}
|
|
32294
|
+
/**
|
|
32295
|
+
* Which nodes get a rendered label, greedily thinned to avoid overlap: a
|
|
32296
|
+
* per-node radius threshold alone ({@link minNodeRadius}) still lets many
|
|
32297
|
+
* same-sized nodes crowded into a small angular wedge all show labels that
|
|
32298
|
+
* visually collide. Nodes are considered in priority order (closer ring
|
|
32299
|
+
* first, then larger on-screen radius) and a label is kept only if its
|
|
32300
|
+
* position isn't too close to an already-kept one.
|
|
32301
|
+
*/
|
|
32302
|
+
#computeLabelKeys(nodes) {
|
|
32303
|
+
const minSpacing = .07;
|
|
32304
|
+
const prioritised = [...nodes.filter((n) => this.#nodeRenderRadius(n) >= this.minNodeRadius)].sort((a, b) => {
|
|
32305
|
+
if (a.ring !== b.ring) return a.ring - b.ring;
|
|
32306
|
+
return Math.hypot(a.position.x, a.position.y) - Math.hypot(b.position.x, b.position.y);
|
|
32307
|
+
});
|
|
32308
|
+
const kept = [];
|
|
32309
|
+
const keys = /* @__PURE__ */ new Set();
|
|
32310
|
+
for (const n of prioritised) if (!kept.some((k) => Math.hypot(k.position.x - n.position.x, k.position.y - n.position.y) < minSpacing)) {
|
|
32311
|
+
kept.push(n);
|
|
32312
|
+
keys.add(n.key);
|
|
32313
|
+
}
|
|
32314
|
+
return keys;
|
|
32315
|
+
}
|
|
32316
|
+
#renderNode(n, siblingGroups, labelKeys) {
|
|
32317
|
+
const { node, key, position } = n;
|
|
32318
|
+
const selected = this._selectedNodes.has(node);
|
|
32319
|
+
const tickled = this.tickled.isTickled(key);
|
|
32320
|
+
const filtered = !this.#matchesFilter(node, n.depth);
|
|
32321
|
+
const pending = this.#pendingKeys.has(key);
|
|
32322
|
+
const isFocused = n.ring === 0;
|
|
32323
|
+
const radius = this.#nodeRenderRadius(n);
|
|
32324
|
+
const showLabel = labelKeys.has(key);
|
|
32325
|
+
const siblings = siblingGroups.get(n.parentKey) ?? [n];
|
|
32326
|
+
const index = siblings.findIndex((s) => s.key === key);
|
|
32327
|
+
const scaledRadius = radius * RENDER_SCALE;
|
|
32328
|
+
const weightStyle = `--node-weight: ${n.weightNormalized};${n.weight === void 0 ? `` : ` --node-weight-raw: ${n.weight};`}`;
|
|
32329
|
+
return w`
|
|
32330
|
+
<g
|
|
32331
|
+
class="node ${selected ? `selected` : ``} ${tickled ? `tickled` : ``} ${filtered ? `filtered` : ``} ${pending ? `loading` : ``} ${isFocused ? `focused` : ``}"
|
|
32332
|
+
data-key=${key}
|
|
32333
|
+
role="treeitem"
|
|
32334
|
+
tabindex="-1"
|
|
32335
|
+
aria-level=${n.depth + 1}
|
|
32336
|
+
aria-selected=${selected}
|
|
32337
|
+
aria-setsize=${siblings.length}
|
|
32338
|
+
aria-posinset=${index + 1}
|
|
32339
|
+
aria-label=${node.item.label}
|
|
32340
|
+
part="node node-${partToken(key)}"
|
|
32341
|
+
style=${weightStyle}
|
|
32342
|
+
transform="translate(${position.x * RENDER_SCALE} ${position.y * RENDER_SCALE})"
|
|
32343
|
+
@pointerdown=${(e) => e.stopPropagation()}
|
|
32344
|
+
@click=${(e) => {
|
|
32345
|
+
e.stopPropagation();
|
|
32346
|
+
this.#handleItemClick(n, e);
|
|
32347
|
+
}}
|
|
32348
|
+
@dblclick=${(e) => {
|
|
32349
|
+
e.stopPropagation();
|
|
32350
|
+
this.#handleItemDblClick(n);
|
|
32351
|
+
}}
|
|
32352
|
+
@pointerenter=${() => this.tickled.handlePointerEnter(key)}
|
|
32353
|
+
>
|
|
32354
|
+
<circle class="node-hit" r=${Math.max(scaledRadius, .035 * RENDER_SCALE)} part="hit" />
|
|
32355
|
+
<circle class="node-dot" r=${scaledRadius} part="dot" />
|
|
32356
|
+
${showLabel ? w`<text class="hyperbolictree-label" part="label" x=${scaledRadius + .01 * RENDER_SCALE} y="0" dominant-baseline="middle">${node.item.label}</text>` : A}
|
|
32357
|
+
</g>
|
|
32358
|
+
`;
|
|
32359
|
+
}
|
|
32360
|
+
render() {
|
|
32361
|
+
const { nodes, edges } = this.#layout();
|
|
32362
|
+
const byKey = new Map(nodes.map((n) => [n.key, n]));
|
|
32363
|
+
const siblingGroups = /* @__PURE__ */ new Map();
|
|
32364
|
+
for (const n of nodes) {
|
|
32365
|
+
const group = siblingGroups.get(n.parentKey) ?? [];
|
|
32366
|
+
group.push(n);
|
|
32367
|
+
siblingGroups.set(n.parentKey, group);
|
|
32368
|
+
}
|
|
32369
|
+
const labelKeys = this.#computeLabelKeys(nodes);
|
|
32370
|
+
return b`
|
|
32371
|
+
<svg
|
|
32372
|
+
class="hyperbolictree"
|
|
32373
|
+
viewBox="${-1.05 * RENDER_SCALE} ${-1.05 * RENDER_SCALE} ${2.1 * RENDER_SCALE} ${2.1 * RENDER_SCALE}"
|
|
32374
|
+
preserveAspectRatio="xMidYMid meet"
|
|
32375
|
+
role="tree"
|
|
32376
|
+
tabindex="0"
|
|
32377
|
+
data-input-mode=${this.tickled.inputMode}
|
|
32378
|
+
@keydown=${(e) => this.#handleKeyDown(e)}
|
|
32379
|
+
@pointerdown=${(e) => this.#handleBackgroundPointerDown(e)}
|
|
32380
|
+
@pointermove=${(e) => this.#handleBackgroundPointerMove(e)}
|
|
32381
|
+
@pointerup=${(e) => this.#handleBackgroundPointerUp(e)}
|
|
32382
|
+
@pointerleave=${() => this.tickled.clearTickled()}
|
|
32383
|
+
>
|
|
32384
|
+
<g class="zoom" transform="scale(${this.zoom})">
|
|
32385
|
+
<circle class="disk-boundary" part="boundary" cx="0" cy="0" r=${RENDER_SCALE} />
|
|
32386
|
+
<g class="edges" part="edges">
|
|
32387
|
+
${repeat(edges, (e) => `${e.fromKey}->${e.toKey}`, (e) => this.#renderEdge(e, byKey))}
|
|
32388
|
+
</g>
|
|
32389
|
+
<g class="nodes" part="nodes">
|
|
32390
|
+
${repeat(nodes, (n) => n.key, (n) => this.#renderNode(n, siblingGroups, labelKeys))}
|
|
32391
|
+
</g>
|
|
32392
|
+
</g>
|
|
32393
|
+
</svg>
|
|
32394
|
+
${this.search.renderField()}
|
|
32395
|
+
`;
|
|
32396
|
+
}
|
|
32397
|
+
static {
|
|
32398
|
+
this.styles = [
|
|
32399
|
+
themeFallbacks,
|
|
32400
|
+
listSearchStyles,
|
|
32401
|
+
i$3`
|
|
32402
|
+
:host {
|
|
32403
|
+
--hyperbolictree-node-color: var(--surface-4);
|
|
32404
|
+
--hyperbolictree-node-text: var(--surface-3-text);
|
|
32405
|
+
--hyperbolictree-edge-color: var(--surface-6);
|
|
32406
|
+
--hyperbolictree-boundary-color: var(--border-subtle);
|
|
32407
|
+
--hyperbolictree-font-size: 25px;
|
|
32408
|
+
|
|
32409
|
+
display: block;
|
|
32410
|
+
overflow: hidden;
|
|
32411
|
+
outline: none;
|
|
32412
|
+
overscroll-behavior: contain;
|
|
32413
|
+
}
|
|
32414
|
+
|
|
32415
|
+
/*
|
|
32416
|
+
* delegatesFocus means the host itself reports as focused whenever
|
|
32417
|
+
* the shadow DOM's focused descendant (the svg) is — without this,
|
|
32418
|
+
* the host also picks up the browser's own default focus ring/fill
|
|
32419
|
+
* sized to the whole box.
|
|
32420
|
+
*/
|
|
32421
|
+
:host(:focus),
|
|
32422
|
+
:host(:focus-visible) {
|
|
32423
|
+
outline: none;
|
|
32424
|
+
}
|
|
32425
|
+
|
|
32426
|
+
.hyperbolictree {
|
|
32427
|
+
display: block;
|
|
32428
|
+
width: 100%;
|
|
32429
|
+
height: 100%;
|
|
32430
|
+
outline: none;
|
|
32431
|
+
touch-action: none;
|
|
32432
|
+
overscroll-behavior: contain;
|
|
32433
|
+
}
|
|
32434
|
+
|
|
32435
|
+
.disk-boundary {
|
|
32436
|
+
fill: none;
|
|
32437
|
+
stroke: var(--hyperbolictree-boundary-color);
|
|
32438
|
+
stroke-width: 1;
|
|
32439
|
+
}
|
|
32440
|
+
|
|
32441
|
+
.edge {
|
|
32442
|
+
fill: none;
|
|
32443
|
+
stroke: var(--hyperbolictree-edge-color);
|
|
32444
|
+
/* --edge-weight (0..1, average of the endpoint nodes' --node-weight) thickens heavier edges. */
|
|
32445
|
+
stroke-width: calc(1px + var(--edge-weight, 0.5) * 3px);
|
|
32446
|
+
}
|
|
32447
|
+
|
|
32448
|
+
.node {
|
|
32449
|
+
cursor: pointer;
|
|
32450
|
+
}
|
|
32451
|
+
|
|
32452
|
+
.node-hit {
|
|
32453
|
+
fill: transparent;
|
|
32454
|
+
pointer-events: all;
|
|
32455
|
+
}
|
|
32456
|
+
|
|
32457
|
+
.node-dot {
|
|
32458
|
+
/* --node-weight (0..1, normalized across the visible node set) tints toward --accent for heavier nodes. */
|
|
32459
|
+
fill: color-mix(in oklab, var(--hyperbolictree-node-color), var(--accent) calc(var(--node-weight, 0.5) * 50%));
|
|
32460
|
+
stroke: var(--surface-2);
|
|
32461
|
+
stroke-width: 1;
|
|
32462
|
+
pointer-events: none;
|
|
32463
|
+
}
|
|
32464
|
+
|
|
32465
|
+
.node.tickled .node-dot {
|
|
32466
|
+
fill: var(--item-bg-tickled);
|
|
32467
|
+
}
|
|
32468
|
+
|
|
32469
|
+
.node.selected .node-dot {
|
|
32470
|
+
fill: var(--item-bg-selected);
|
|
32471
|
+
}
|
|
32472
|
+
|
|
32473
|
+
.node.selected.tickled .node-dot {
|
|
32474
|
+
fill: var(--item-bg-selected-tickled);
|
|
32475
|
+
}
|
|
32476
|
+
|
|
32477
|
+
.node.focused .node-dot {
|
|
32478
|
+
stroke: var(--accent);
|
|
32479
|
+
stroke-width: 3;
|
|
32480
|
+
}
|
|
32481
|
+
|
|
32482
|
+
.node.filtered {
|
|
32483
|
+
opacity: 0.3;
|
|
32484
|
+
}
|
|
32485
|
+
|
|
32486
|
+
.node.loading .node-dot {
|
|
32487
|
+
opacity: 0.6;
|
|
32488
|
+
}
|
|
32489
|
+
|
|
32490
|
+
.hyperbolictree-label {
|
|
32491
|
+
font-size: var(--hyperbolictree-font-size);
|
|
32492
|
+
fill: var(--hyperbolictree-node-text);
|
|
32493
|
+
cursor: pointer;
|
|
32494
|
+
user-select: none;
|
|
32495
|
+
}
|
|
32496
|
+
`
|
|
32497
|
+
];
|
|
32498
|
+
}
|
|
32499
|
+
};
|
|
32500
|
+
__decorate([property({ type: Object })], HyperbolicTreeListElement.prototype, "root", void 0);
|
|
32501
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "model", void 0);
|
|
32502
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "loadChildren", void 0);
|
|
32503
|
+
__decorate([property({
|
|
32504
|
+
reflect: true,
|
|
32505
|
+
attribute: `exclusivity`
|
|
32506
|
+
})], HyperbolicTreeListElement.prototype, "exclusivity", void 0);
|
|
32507
|
+
__decorate([property({
|
|
32508
|
+
reflect: true,
|
|
32509
|
+
attribute: `selection-filter`
|
|
32510
|
+
})], HyperbolicTreeListElement.prototype, "selectionFilter", void 0);
|
|
32511
|
+
__decorate([property({
|
|
32512
|
+
type: String,
|
|
32513
|
+
attribute: `interaction-mode`
|
|
32514
|
+
})], HyperbolicTreeListElement.prototype, "interactionMode", void 0);
|
|
32515
|
+
__decorate([property({
|
|
32516
|
+
type: Number,
|
|
32517
|
+
attribute: `max-radius`
|
|
32518
|
+
})], HyperbolicTreeListElement.prototype, "maxRadius", void 0);
|
|
32519
|
+
__decorate([property({
|
|
32520
|
+
type: Number,
|
|
32521
|
+
attribute: `node-radius`
|
|
32522
|
+
})], HyperbolicTreeListElement.prototype, "nodeRadius", void 0);
|
|
32523
|
+
__decorate([property({
|
|
32524
|
+
type: Number,
|
|
32525
|
+
attribute: `min-node-radius`
|
|
32526
|
+
})], HyperbolicTreeListElement.prototype, "minNodeRadius", void 0);
|
|
32527
|
+
__decorate([property({
|
|
32528
|
+
type: Number,
|
|
32529
|
+
attribute: `min-weight-scale`
|
|
32530
|
+
})], HyperbolicTreeListElement.prototype, "minWeightScale", void 0);
|
|
32531
|
+
__decorate([property({
|
|
32532
|
+
type: Number,
|
|
32533
|
+
attribute: `max-weight-scale`
|
|
32534
|
+
})], HyperbolicTreeListElement.prototype, "maxWeightScale", void 0);
|
|
32535
|
+
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "zoom", void 0);
|
|
32536
|
+
__decorate([property({ type: Number })], HyperbolicTreeListElement.prototype, "curvature", void 0);
|
|
32537
|
+
__decorate([property({
|
|
32538
|
+
type: Number,
|
|
32539
|
+
attribute: `focus-animation-duration`
|
|
32540
|
+
})], HyperbolicTreeListElement.prototype, "focusAnimationDuration", void 0);
|
|
32541
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "edgeRenderer", void 0);
|
|
32542
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "selectedNode", void 0);
|
|
32543
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_selectedNodes", void 0);
|
|
32544
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "filterPredicate", void 0);
|
|
32545
|
+
__decorate([property({
|
|
32546
|
+
type: Boolean,
|
|
32547
|
+
attribute: `disable-keyboard-navigation`
|
|
32548
|
+
})], HyperbolicTreeListElement.prototype, "disableKeyboardNavigation", void 0);
|
|
32549
|
+
__decorate([property({ attribute: false })], HyperbolicTreeListElement.prototype, "registry", void 0);
|
|
32550
|
+
__decorate([property({
|
|
32551
|
+
type: String,
|
|
32552
|
+
attribute: `search-modes`,
|
|
32553
|
+
reflect: true
|
|
32554
|
+
})], HyperbolicTreeListElement.prototype, "searchModes", void 0);
|
|
32555
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_focusKey", void 0);
|
|
32556
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_animCenter", void 0);
|
|
32557
|
+
__decorate([state()], HyperbolicTreeListElement.prototype, "_panOffset", void 0);
|
|
32558
|
+
HyperbolicTreeListElement = __decorate([safeCustomElement(`ixfx-hyperbolictree-list`)], HyperbolicTreeListElement);
|
|
32559
|
+
//#endregion
|
|
32560
|
+
//#region src/icicle-tree/layout.ts
|
|
32561
|
+
/** Coerce a raw accessor result to a non-negative finite weight. */
|
|
32562
|
+
function normaliseValue(raw) {
|
|
32563
|
+
return typeof raw === `number` && Number.isFinite(raw) && raw > 0 ? raw : 0;
|
|
32564
|
+
}
|
|
32565
|
+
/** Depth-first search for the node with `key`. Returns `undefined` when absent. */
|
|
32566
|
+
function findIcicleNode(root, key) {
|
|
32567
|
+
if (!root) return void 0;
|
|
32568
|
+
if (root.item.key === key) return root;
|
|
32569
|
+
for (const child of root.children ?? []) {
|
|
32570
|
+
const hit = findIcicleNode(child, key);
|
|
32571
|
+
if (hit) return hit;
|
|
32572
|
+
}
|
|
32573
|
+
}
|
|
32574
|
+
/**
|
|
32575
|
+
* Chain of nodes from `root` down to (and including) the node with `key`.
|
|
32576
|
+
* Returns `undefined` when the key is absent. The first entry is always `root`.
|
|
32577
|
+
*/
|
|
32578
|
+
function findIciclePath(root, key) {
|
|
32579
|
+
if (!root) return void 0;
|
|
32580
|
+
if (root.item.key === key) return [root];
|
|
32581
|
+
for (const child of root.children ?? []) {
|
|
32582
|
+
const hit = findIciclePath(child, key);
|
|
32583
|
+
if (hit) return [root, ...hit];
|
|
32584
|
+
}
|
|
32585
|
+
}
|
|
32586
|
+
/**
|
|
32587
|
+
* Compute the visible rectangles for an icicle view.
|
|
32588
|
+
*
|
|
32589
|
+
* Bands start at the zoom node's **children** (the zoom node itself is shown in
|
|
32590
|
+
* the breadcrumb strip, never as a rect — matching `TreeComponent.getNodes()`,
|
|
32591
|
+
* where the root is never listed). Each band spans 0–1; a rect's `x`/`width`
|
|
32592
|
+
* are fractions of its band, proportional to `valueAccessor` shares among
|
|
32593
|
+
* siblings. Siblings whose weights all normalise to `0` split the band equally.
|
|
32594
|
+
*
|
|
32595
|
+
* @param root True tree root. `undefined` (or a root without children) yields `[]`.
|
|
32596
|
+
* @param options `valueAccessor` (default equal weights), `maxDepth` (band count,
|
|
32597
|
+
* default unlimited), `zoomKey` (visual root, default true root; unknown keys
|
|
32598
|
+
* fall back to the true root).
|
|
32599
|
+
*/
|
|
32600
|
+
function computeIcicleLayout(root, options = {}) {
|
|
32601
|
+
if (!root?.children?.length) return [];
|
|
32602
|
+
const { maxDepth = Number.POSITIVE_INFINITY, valueAccessor = () => 1, zoomKey, hideLeaves = false } = options;
|
|
32603
|
+
if (maxDepth <= 0) return [];
|
|
32604
|
+
const zoomNode = zoomKey === void 0 ? root : findIcicleNode(root, zoomKey) ?? root;
|
|
32605
|
+
const zoomPath = findIciclePath(root, zoomNode.item.key);
|
|
32606
|
+
const zoomDepth = zoomPath ? zoomPath.length - 2 : -1;
|
|
32607
|
+
const siblings = zoomNode.children ?? [];
|
|
32608
|
+
if (siblings.length === 0) return [];
|
|
32609
|
+
const isLeaf = (node) => node.item.isLeaf === true || node.children !== void 0 && node.children.length === 0;
|
|
32610
|
+
const rects = [];
|
|
32611
|
+
const layoutLevel = (nodes, depth, relativeDepth, offset, span, parentKey) => {
|
|
32612
|
+
const visible = hideLeaves ? nodes.filter((n) => !isLeaf(n)) : nodes;
|
|
32613
|
+
if (visible.length === 0 || relativeDepth >= maxDepth) return;
|
|
32614
|
+
const weights = visible.map((n) => normaliseValue(valueAccessor(n.item)));
|
|
32615
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
32616
|
+
const setSize = visible.length;
|
|
32617
|
+
let cursor = offset;
|
|
32618
|
+
visible.forEach((node, index) => {
|
|
32619
|
+
const width = span * (total > 0 ? weights[index] / total : 1 / setSize);
|
|
32620
|
+
const children = node.children ?? [];
|
|
32621
|
+
rects.push({
|
|
32622
|
+
node,
|
|
32623
|
+
depth,
|
|
32624
|
+
relativeDepth,
|
|
32625
|
+
x: cursor,
|
|
32626
|
+
width,
|
|
32627
|
+
parentKey,
|
|
32628
|
+
index,
|
|
32629
|
+
setSize,
|
|
32630
|
+
hasChildren: children.length > 0 || node.children === void 0 && node.item.isLeaf !== true
|
|
32631
|
+
});
|
|
32632
|
+
if (children.length > 0) layoutLevel(children, depth + 1, relativeDepth + 1, cursor, width, node.item.key);
|
|
32633
|
+
cursor += width;
|
|
32634
|
+
});
|
|
32635
|
+
};
|
|
32636
|
+
layoutLevel(siblings, zoomDepth + 1, 0, 0, 1, zoomNode.item.key);
|
|
32637
|
+
return rects;
|
|
32638
|
+
}
|
|
32639
|
+
//#endregion
|
|
32640
|
+
//#region src/icicle-tree/icicletree-list.ts
|
|
32641
|
+
let IcicleTreeListElement = class IcicleTreeListElement extends i {
|
|
32642
|
+
constructor(..._args) {
|
|
32643
|
+
super(..._args);
|
|
32644
|
+
this.exclusivity = `none`;
|
|
32645
|
+
this.selectionFilter = `all`;
|
|
32646
|
+
this.interactionMode = `standard`;
|
|
32647
|
+
this.orientation = `horizontal`;
|
|
32648
|
+
this.maxDepth = 4;
|
|
32649
|
+
this.minLabelWidth = 40;
|
|
32650
|
+
this.minVerticalLabelWidth = 16;
|
|
32651
|
+
this.hideLeaves = false;
|
|
32652
|
+
this.valueAccessor = () => 1;
|
|
32653
|
+
this._selectedNodes = /* @__PURE__ */ new Set();
|
|
32654
|
+
this.disableKeyboardNavigation = false;
|
|
32655
|
+
this.registry = void 0;
|
|
32656
|
+
this._zoomKey = void 0;
|
|
32657
|
+
this.search = new ListSearchController(this, { adapter: createTreeListSearchAdapter(this, {
|
|
32658
|
+
labelSelector: `.icicle-label`,
|
|
32659
|
+
highlightKey: `incr-search-highlight`
|
|
32660
|
+
}) });
|
|
32661
|
+
this.tickled = new TickledController(this, {
|
|
32662
|
+
getItems: () => this.#rects().map((r) => ({
|
|
32663
|
+
id: r.node.item.key,
|
|
32664
|
+
enabled: true
|
|
32665
|
+
})),
|
|
32666
|
+
onStateChange: () => {
|
|
32667
|
+
this.setAttribute(`data-input-mode`, this.tickled.inputMode);
|
|
32668
|
+
},
|
|
32669
|
+
onTickle: (id) => {
|
|
32670
|
+
if (id === void 0) return;
|
|
32671
|
+
const rect = this.#rects().find((r) => r.node.item.key === id);
|
|
32672
|
+
if (!rect) return;
|
|
32673
|
+
this.dispatchEvent(new CustomEvent(`tickle`, {
|
|
32674
|
+
detail: {
|
|
32675
|
+
node: rect.node,
|
|
32676
|
+
depth: rect.depth
|
|
32677
|
+
},
|
|
32678
|
+
bubbles: true,
|
|
32679
|
+
composed: true
|
|
32680
|
+
}));
|
|
32681
|
+
}
|
|
32682
|
+
});
|
|
32683
|
+
this.#pendingKeys = /* @__PURE__ */ new Set();
|
|
32684
|
+
this.#aborters = /* @__PURE__ */ new Map();
|
|
32685
|
+
}
|
|
32686
|
+
static {
|
|
32687
|
+
this.shadowRootOptions = {
|
|
32688
|
+
...i.shadowRootOptions,
|
|
32689
|
+
delegatesFocus: true
|
|
32690
|
+
};
|
|
32691
|
+
}
|
|
32692
|
+
get selectedNodes() {
|
|
32693
|
+
return this._selectedNodes;
|
|
32694
|
+
}
|
|
32695
|
+
/** Current zoom root. `undefined` when showing the full tree. Pure view-state — `root` is never mutated. */
|
|
32696
|
+
get focusedNode() {
|
|
32697
|
+
if (this._zoomKey === void 0) return void 0;
|
|
32698
|
+
return findIcicleNode(this.#effectiveRoot, this._zoomKey);
|
|
32699
|
+
}
|
|
32700
|
+
#modelUnsubscribe;
|
|
32701
|
+
#pendingKeys;
|
|
32702
|
+
#aborters;
|
|
32703
|
+
#anchorIndex;
|
|
32704
|
+
#ownedRegistry;
|
|
32705
|
+
#unregisterSearchCommands;
|
|
32706
|
+
#resizeObserver;
|
|
32707
|
+
get #effectiveRoot() {
|
|
32708
|
+
return this.model?.root ?? this.root;
|
|
32709
|
+
}
|
|
32710
|
+
#rects() {
|
|
32711
|
+
return computeIcicleLayout(this.#effectiveRoot, {
|
|
32712
|
+
valueAccessor: this.valueAccessor,
|
|
32713
|
+
maxDepth: this.maxDepth,
|
|
32714
|
+
zoomKey: this._zoomKey,
|
|
32715
|
+
hideLeaves: this.hideLeaves
|
|
32716
|
+
});
|
|
32717
|
+
}
|
|
32718
|
+
connectedCallback() {
|
|
32719
|
+
super.connectedCallback();
|
|
32720
|
+
this.setAttribute(`data-input-mode`, `mouse`);
|
|
32721
|
+
const registry = this.#ownedRegistry = createCommandRegistry();
|
|
32722
|
+
registry.register({
|
|
32723
|
+
id: `select-all`,
|
|
32724
|
+
label: `Select All`,
|
|
32725
|
+
description: `Select all visible items`,
|
|
32726
|
+
execute: () => this.selectAll()
|
|
32727
|
+
});
|
|
32728
|
+
registry.register({
|
|
32729
|
+
id: `deselect-all`,
|
|
32730
|
+
label: `Deselect All`,
|
|
32731
|
+
description: `Clear all selection`,
|
|
32732
|
+
execute: () => this.deselectAll()
|
|
32733
|
+
});
|
|
32734
|
+
this.registry = registry;
|
|
32735
|
+
this.#syncSearchCommands();
|
|
32736
|
+
if (typeof ResizeObserver !== `undefined`) {
|
|
32737
|
+
this.#resizeObserver = new ResizeObserver(() => this.requestUpdate());
|
|
32738
|
+
this.#resizeObserver.observe(this);
|
|
32739
|
+
}
|
|
32740
|
+
}
|
|
32741
|
+
disconnectedCallback() {
|
|
32742
|
+
super.disconnectedCallback();
|
|
32743
|
+
this.#modelUnsubscribe?.();
|
|
32744
|
+
this.#modelUnsubscribe = void 0;
|
|
32745
|
+
for (const aborter of this.#aborters.values()) aborter.abort();
|
|
32746
|
+
this.#aborters.clear();
|
|
32747
|
+
this.#pendingKeys.clear();
|
|
32748
|
+
this.#resizeObserver?.disconnect();
|
|
32749
|
+
this.#resizeObserver = void 0;
|
|
32750
|
+
if (this.#ownedRegistry) {
|
|
32751
|
+
this.#ownedRegistry.unregister(`select-all`);
|
|
32752
|
+
this.#ownedRegistry.unregister(`deselect-all`);
|
|
32753
|
+
this.#ownedRegistry = void 0;
|
|
32754
|
+
this.registry = void 0;
|
|
32755
|
+
}
|
|
32756
|
+
}
|
|
32757
|
+
#effectiveSearchModes() {
|
|
32758
|
+
const raw = this.searchModes?.trim();
|
|
32759
|
+
if (raw === void 0) return [...LIST_SEARCH_MODES];
|
|
32760
|
+
const requested = new Set(raw.split(/\s+/));
|
|
32761
|
+
return LIST_SEARCH_MODES.filter((m) => requested.has(m));
|
|
32762
|
+
}
|
|
32763
|
+
#syncSearchCommands() {
|
|
32764
|
+
if (!this.#ownedRegistry) return;
|
|
32765
|
+
this.search.setModes(this.#effectiveSearchModes());
|
|
32766
|
+
this.#unregisterSearchCommands?.();
|
|
32767
|
+
this.#unregisterSearchCommands = registerListSearchCommands(this.#ownedRegistry, this.search);
|
|
32768
|
+
}
|
|
32769
|
+
updated(changedProps) {
|
|
32770
|
+
if (changedProps.has(`root`) && this.root) this.#resetViewState();
|
|
32771
|
+
if (changedProps.has(`model`)) {
|
|
32772
|
+
this.#modelUnsubscribe?.();
|
|
32773
|
+
this.#modelUnsubscribe = void 0;
|
|
32774
|
+
if (this.model) {
|
|
32775
|
+
this.#modelUnsubscribe = this.model._subscribe((event) => {
|
|
32776
|
+
if (event.type === `root`) {
|
|
32777
|
+
this.#resetViewState();
|
|
32778
|
+
this.requestUpdate();
|
|
32779
|
+
} else if (event.type === `invalidate`) this.requestUpdate();
|
|
32780
|
+
});
|
|
32781
|
+
this.#resetViewState();
|
|
32782
|
+
}
|
|
32783
|
+
}
|
|
32784
|
+
if (changedProps.has(`interactionMode`)) {
|
|
32785
|
+
this.#anchorIndex = void 0;
|
|
32786
|
+
this.clearSelection();
|
|
32787
|
+
}
|
|
32788
|
+
if (changedProps.has(`searchModes`)) this.#syncSearchCommands();
|
|
32789
|
+
if (this._zoomKey !== void 0 && !findIcicleNode(this.#effectiveRoot, this._zoomKey)) this._zoomKey = void 0;
|
|
32790
|
+
this.#ensureVisibleChildren();
|
|
32791
|
+
this.#syncTooltip();
|
|
32792
|
+
}
|
|
32793
|
+
/** The host's own (viewport) extent — width in horizontal orientation, height in vertical. */
|
|
32794
|
+
#hostExtent() {
|
|
32795
|
+
return (this.orientation === `vertical` ? this.clientHeight : this.clientWidth) || 0;
|
|
32796
|
+
}
|
|
32797
|
+
/**
|
|
32798
|
+
* Whether `rect`'s label should be rotated or hidden for lack of space, per
|
|
32799
|
+
* {@link minLabelWidth} / {@link minVerticalLabelWidth}. `orientation="vertical"`
|
|
32800
|
+
* rects never rotate — hides outright at `minLabelWidth` instead, same as before.
|
|
32801
|
+
*/
|
|
32802
|
+
#labelMode(rect) {
|
|
32803
|
+
const hostExtent = this.#hostExtent();
|
|
32804
|
+
if (hostExtent === 0) return {
|
|
32805
|
+
hidden: false,
|
|
32806
|
+
rotated: false
|
|
32807
|
+
};
|
|
32808
|
+
const vertical = this.orientation === `vertical`;
|
|
32809
|
+
const pixelWidth = rect.width * hostExtent;
|
|
32810
|
+
if (vertical) return {
|
|
32811
|
+
hidden: pixelWidth < this.minLabelWidth,
|
|
32812
|
+
rotated: false
|
|
32813
|
+
};
|
|
32814
|
+
if (pixelWidth < this.minVerticalLabelWidth) return {
|
|
32815
|
+
hidden: true,
|
|
32816
|
+
rotated: false
|
|
32817
|
+
};
|
|
32818
|
+
return {
|
|
32819
|
+
hidden: false,
|
|
32820
|
+
rotated: pixelWidth < this.minLabelWidth
|
|
32821
|
+
};
|
|
32822
|
+
}
|
|
32823
|
+
/** Anchor the shared tooltip popover to the tickled rect, if its label needs one. */
|
|
32824
|
+
#syncTooltip() {
|
|
32825
|
+
const popover = this._tooltipEl;
|
|
32826
|
+
if (!popover?.popover) return;
|
|
32827
|
+
const key = this.tickled.tickledId;
|
|
32828
|
+
const rect = key === void 0 ? void 0 : this.#rects().find((r) => r.node.item.key === key);
|
|
32829
|
+
const mode = rect ? this.#labelMode(rect) : void 0;
|
|
32830
|
+
const isOpen = popover.matches(`:popover-open`);
|
|
32831
|
+
if (rect && mode && (mode.hidden || mode.rotated)) {
|
|
32832
|
+
popover.textContent = rect.node.item.label;
|
|
32833
|
+
if (!isOpen) try {
|
|
32834
|
+
popover.showPopover();
|
|
32835
|
+
} catch {}
|
|
32836
|
+
} else if (isOpen) try {
|
|
32837
|
+
popover.hidePopover();
|
|
32838
|
+
} catch {}
|
|
32839
|
+
}
|
|
32840
|
+
#resetViewState() {
|
|
32841
|
+
this.#clearSelectionInternal();
|
|
32842
|
+
this.tickled.clearTickled();
|
|
32843
|
+
this._zoomKey = void 0;
|
|
32844
|
+
this.#anchorIndex = void 0;
|
|
32845
|
+
}
|
|
32846
|
+
#ensureVisibleChildren() {
|
|
32847
|
+
if (!this.model && !this.loadChildren) return;
|
|
32848
|
+
for (const rect of this.#rects()) {
|
|
32849
|
+
const { node } = rect;
|
|
32850
|
+
if (node.children !== void 0 || this.#pendingKeys.has(node.item.key)) continue;
|
|
32851
|
+
this.#loadChildren(node, rect.depth);
|
|
31489
32852
|
}
|
|
31490
32853
|
}
|
|
31491
32854
|
async #loadChildren(node, depth) {
|
|
@@ -37590,8 +38953,7 @@ let MulticolListElement = class MulticolListElement extends i {
|
|
|
37590
38953
|
display: flex;
|
|
37591
38954
|
flex-direction: row;
|
|
37592
38955
|
align-items: stretch;
|
|
37593
|
-
overflow
|
|
37594
|
-
overflow-y: hidden;
|
|
38956
|
+
overflow: hidden;
|
|
37595
38957
|
position: relative;
|
|
37596
38958
|
height: 100%;
|
|
37597
38959
|
}
|
|
@@ -38169,6 +39531,71 @@ let MulticolListElement = class MulticolListElement extends i {
|
|
|
38169
39531
|
composed: true
|
|
38170
39532
|
}));
|
|
38171
39533
|
}
|
|
39534
|
+
async autoSizeColumns(options) {
|
|
39535
|
+
return new Promise((resolve) => {
|
|
39536
|
+
setTimeout(() => {
|
|
39537
|
+
const minWidth = options?.minWidth ?? MIN_COLUMN_WIDTH;
|
|
39538
|
+
const maxWidth = options?.maxWidth ?? MAX_COLUMN_WIDTH;
|
|
39539
|
+
const measure = (text) => {
|
|
39540
|
+
const span = document.createElement(`span`);
|
|
39541
|
+
span.style.position = `absolute`;
|
|
39542
|
+
span.style.visibility = `hidden`;
|
|
39543
|
+
span.style.whiteSpace = `nowrap`;
|
|
39544
|
+
span.style.font = getComputedStyle(this).font;
|
|
39545
|
+
span.textContent = text;
|
|
39546
|
+
document.body.appendChild(span);
|
|
39547
|
+
const w = span.getBoundingClientRect().width;
|
|
39548
|
+
document.body.removeChild(span);
|
|
39549
|
+
return w;
|
|
39550
|
+
};
|
|
39551
|
+
for (const col of this._columns) {
|
|
39552
|
+
const colEl = this.shadowRoot?.querySelector(`[data-col-id="${col.id}"]`);
|
|
39553
|
+
if (!colEl) continue;
|
|
39554
|
+
const headerEl = colEl.querySelector(`.column-header`);
|
|
39555
|
+
const listerHost = colEl.querySelector(`.lister-host`);
|
|
39556
|
+
let contentWidth = 0;
|
|
39557
|
+
if (listerHost) {
|
|
39558
|
+
const allLabel = listerHost.querySelector(`.all-label`);
|
|
39559
|
+
if (allLabel) contentWidth = Math.max(contentWidth, measure(allLabel.textContent ?? ``));
|
|
39560
|
+
const groupedLister = listerHost.querySelector(`ixfx-grouped-item-lister`);
|
|
39561
|
+
if (groupedLister) {
|
|
39562
|
+
const verticalLists = groupedLister.querySelectorAll(`ixfx-vertical-list`);
|
|
39563
|
+
for (const vl of verticalLists) {
|
|
39564
|
+
const itemEls = vl.querySelectorAll(`li`);
|
|
39565
|
+
for (const li of itemEls) {
|
|
39566
|
+
const labelEl = li.querySelector(`.item-label`);
|
|
39567
|
+
const countEl = li.querySelector(`.count`);
|
|
39568
|
+
const combinedText = (labelEl?.textContent ?? ``) + (countEl?.textContent ?? ``);
|
|
39569
|
+
contentWidth = Math.max(contentWidth, measure(combinedText));
|
|
39570
|
+
}
|
|
39571
|
+
}
|
|
39572
|
+
}
|
|
39573
|
+
}
|
|
39574
|
+
let headerWidth = 0;
|
|
39575
|
+
if (headerEl) {
|
|
39576
|
+
const labelBtn = headerEl.querySelector(`.column-label`);
|
|
39577
|
+
if (labelBtn) {
|
|
39578
|
+
const labelTextWidth = measure(labelBtn.textContent ?? ``);
|
|
39579
|
+
const btnStyle = getComputedStyle(labelBtn);
|
|
39580
|
+
const labelBtnPaddingH = Number.parseFloat(btnStyle.paddingLeft) + Number.parseFloat(btnStyle.paddingRight);
|
|
39581
|
+
const burgerBtn = headerEl.querySelector(`.burger-btn`);
|
|
39582
|
+
const burgerWidth = burgerBtn ? Number.parseFloat(getComputedStyle(burgerBtn).width) : 0;
|
|
39583
|
+
const headerStyle = getComputedStyle(headerEl);
|
|
39584
|
+
const gap = Number.parseFloat(headerStyle.gap) || 0;
|
|
39585
|
+
headerWidth = labelTextWidth + labelBtnPaddingH + burgerWidth + gap;
|
|
39586
|
+
}
|
|
39587
|
+
}
|
|
39588
|
+
const neededWidth = Math.max(headerWidth, contentWidth + 24 + 16);
|
|
39589
|
+
const clampedWidth = Math.max(minWidth, Math.min(maxWidth, Math.ceil(neededWidth)));
|
|
39590
|
+
col.setWidth(clampedWidth);
|
|
39591
|
+
}
|
|
39592
|
+
this._updateColOffsets();
|
|
39593
|
+
this._emitColumnsChange(`resize`);
|
|
39594
|
+
this.requestUpdate();
|
|
39595
|
+
resolve();
|
|
39596
|
+
}, 0);
|
|
39597
|
+
});
|
|
39598
|
+
}
|
|
38172
39599
|
};
|
|
38173
39600
|
__decorate([property({ attribute: false })], MulticolListElement.prototype, "categories", void 0);
|
|
38174
39601
|
__decorate([property({ attribute: false })], MulticolListElement.prototype, "dataProvider", void 0);
|
|
@@ -50537,6 +51964,6 @@ function init() {
|
|
|
50537
51964
|
console.log(`Init`);
|
|
50538
51965
|
}
|
|
50539
51966
|
//#endregion
|
|
50540
|
-
export { AcTextElement, AcTokenElement, BottomDrawerElement, 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, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeColour, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, dropTargetFor, extractStack, findIcicleNode, findIciclePath, findNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, pointerOverAnyHost, prefersReducedMotion, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, walk, wrapRoot };
|
|
51967
|
+
export { AcTextElement, AcTokenElement, BottomDrawerElement, ButtonElement, Checkbox, ColourPicker, ColourPickerPopup, CrumbNavigationElement, CrumbPathController, CrumbPathElement, DataController, DataDisplayComponent, DataGridElement, DataTableElement, DetailListElement, DockHostElement, DockPanelElement, EditableLabel, EditableNumber, FormElement, FormGroupElement, FormPartElement, GridListElement, GroupedItemListerElement, HyperbolicTreeListElement, ICON_CARET_RIGHT, ICON_CHECK, ICON_CHEVRON_DOWN, ICON_CLOSE, IcicleTreeListElement, IncrSearchTreeController as IncrSearchMillerController, IncrSearchTreeController, IxfxHexEditorElement, IxfxIconElement, IxfxTimelineElement, KeyedElementIndex, LIST_ITEM_MOTION_MS, LIST_SEARCH_COMMAND_IDS, LIST_SEARCH_MODES, LabelledRadialInput, LabelledRangeInput, LabelledSliderInput, LedElement, ListDragSelectController, ListNativeDragController, ListSearchController, ListSelectionController, menu_exports as Menu, MillerBaseElement, MillerListElement, MillerTreeController, MulticolListElement, NarrowedTextElement, NotificationManager, NotificationPillElement, PanTextElement, PanelElement, PanelGroupElement, PlotDataSeries, PlotHistogram, PlotMultiAxis, PlotSingleAxis, PlotXyAxis, PolarPad, RadialInputElement, RadioGroup, RangeElement, RangeInput, RangeMultiElement, RatingElement, SelectHorizElement, ShapefillValueElement, SliderInputElement, SlidingNumberElement, SnackbarElement, SnapContainer, SplitButton, SplitLayoutElement, SplitPanelElement, SwipeElement, TabController, TabListElement, TabListItemElement, TabPanelElement, TabPanelsElement, TextShimmerElement, TickledController, TimelineController, TimelineTrackTabElement, TitlebarElement, ToolbarElement, ToolbarGroupElement, TooltipElement, TransitoryLabelElement, TreeBaseElement, TreeController, TreeDataModel, TreeDropController, TreeKeyboardNavController, TreeListElement, TreemapListElement, TwoSplitLayoutElement, VerticalListElement, XyPad, addPanelToStack, animateListItemIn, animateListItemOut, applyAgeModification, applyHighlights, applySnapshot, applyTreeDrop, attachTimelineInteractions, bottomDrawerCommandId, buildHighlightTargets, buildModel, captionForEntry, clampToDisk, clearHighlights, collectContainerNames, collectDataResult, collectPanelIds, collectStackPaths, computeCanonicalPositions, computeColour, computeHyperbolicLayout, computeIcicleLayout, computeLeafGeoms, computeTreemapLayout, createAcCompletionProvider, createDataListSearchAdapter, createDefaultListDragPreview, createElementIncrSearch, createElementListSearchAdapter, createEmptyLayout, createEmptyStack, createFuzzySearch, createIncrSearch, createPillRenderer, createSplit, createStack, createTreeListAdapter, createTreeListSearchAdapter, createVerticalListAdapter, distribute, dropTargetFor, euclideanRadiusForHyperbolicDistance, extractStack, findHyperbolicNode, findHyperbolicPath, findIcicleNode, findIciclePath, findNode, findPanelNode, findParentOf, findStackOf, findStackPathByName, findTreemapNode, findTreemapPath, geodesicInterpolate, getActiveDragPanelId, getIcon, getIconEntry, getIconFont, getLeafNeighbours, hasIcon, hyperbolicDistanceForEuclideanRadius, iconBus, init, insertBeside, isDragging, isMultiMode, isSplit, isStack, keyForElement, listItemFadeKeyframes, listSearchStyles, mobiusAdd, mobiusFromOrigin, mobiusToOrigin, monitorIndeterminate, moveLeafNodeToEdge, moveLeafStep, norm, normalise, normaliseValue, normalize, panelGapIndex, parseColourScaleAttr, parseLayout, poincareDistance, pointerOverAnyHost, prefersReducedMotion, preserveRestoreWeights, pruneDraggedDescendants, readBaseColour, registerFontIcon, registerIcon, registerListSearchCommands, removePanelFromStack, resolveCssVars, resolveGrid, resolveLeafPath, resolveTracks, serializeLayout, setActive, setIconFont, setPanelRestoreWeight, startListItemOutAnimation, svgToDataUri, swapLeaves, tickledItemStyles, tickledStyles, timelineStyles, toSnapshot, toolbarGapIndex, walk, wrapRoot };
|
|
50541
51968
|
|
|
50542
51969
|
//# sourceMappingURL=index.js.map
|