124 lines
4.2 KiB
JavaScript
124 lines
4.2 KiB
JavaScript
export const TreeRevealMixin = (superClass) => class TreeRevealMixin extends superClass {
|
|
async scrollToKey(key) {
|
|
await this.updateComplete;
|
|
const idx = this._findFlatIndexByKey(key);
|
|
if (idx < 0) return false;
|
|
const scroller = this.shadowRoot?.querySelector('.tree');
|
|
if (scroller?.scrollToIndex) {
|
|
scroller.scrollToIndex(idx, { block: 'center' });
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Expand+load ancestors so `path` becomes visible, then optionally select/scroll it.
|
|
* `path` is a key path (slug path), like `root/dir/file`.
|
|
*/
|
|
async revealPath(path, options = {}) {
|
|
const segments = this._normalizeKeyPath(path);
|
|
if (!segments.length) return { found: false, key: '', node: undefined };
|
|
|
|
const select = options.select !== false;
|
|
const scroll = options.scroll !== false;
|
|
const open = options.open === true;
|
|
const source = options.source ?? 'revealPath';
|
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : 15000;
|
|
|
|
if (this._revealController) {
|
|
try { this._revealController.abort(); } catch (e) { }
|
|
}
|
|
const controller = new AbortController();
|
|
this._revealController = controller;
|
|
|
|
const externalSignal = options.signal;
|
|
if (externalSignal?.aborted) return { found: false, key: segments.join('/') };
|
|
const abortIfExternal = () => {
|
|
try { controller.abort(); } catch (e) { }
|
|
};
|
|
externalSignal?.addEventListener?.('abort', abortIfExternal, { once: true });
|
|
|
|
const start = performance.now();
|
|
const ensureNotTimedOut = () => {
|
|
if (timeoutMs <= 0) return;
|
|
if (performance.now() - start > timeoutMs) {
|
|
const err = new Error('revealPath timeout');
|
|
err.name = 'TimeoutError';
|
|
throw err;
|
|
}
|
|
};
|
|
|
|
const ensureExpanded = (key) => {
|
|
if (!this.manageState) return;
|
|
const set = new Set(this._expandedPathSet);
|
|
if (!set.has(key)) {
|
|
set.add(key);
|
|
this._expandedPathSet = set;
|
|
this.expandedPaths = Array.from(set);
|
|
this._rebuildManagedTree();
|
|
}
|
|
};
|
|
|
|
const ensureSelected = (key) => {
|
|
if (!this.manageState) return;
|
|
const set = new Set();
|
|
set.add(key);
|
|
this._selectedPathSet = set;
|
|
this.selectedPaths = Array.from(set);
|
|
this._rebuildManagedTree();
|
|
};
|
|
|
|
let currentKey = '';
|
|
for (let i = 0; i < segments.length; i++) {
|
|
ensureNotTimedOut();
|
|
if (controller.signal.aborted) break;
|
|
|
|
currentKey = currentKey ? `${currentKey}/${segments[i]}` : segments[i];
|
|
const node = this._findNodeByKey(currentKey);
|
|
|
|
if (!node) {
|
|
externalSignal?.removeEventListener?.('abort', abortIfExternal);
|
|
return { found: false, key: currentKey, node: undefined };
|
|
}
|
|
|
|
const isLeaf = i === segments.length - 1;
|
|
ensureExpanded(currentKey);
|
|
|
|
if (!isLeaf && typeof this.loadChildren === 'function') {
|
|
await this._loadChildrenForExpanded(currentKey, source, null, node, currentKey.split('/'));
|
|
if (controller.signal.aborted) break;
|
|
const nextKey = `${currentKey}/${segments[i + 1]}`;
|
|
if (!this._findNodeByKey(nextKey)) {
|
|
externalSignal?.removeEventListener?.('abort', abortIfExternal);
|
|
return { found: false, key: nextKey, node: undefined };
|
|
}
|
|
}
|
|
}
|
|
|
|
if (controller.signal.aborted) {
|
|
externalSignal?.removeEventListener?.('abort', abortIfExternal);
|
|
return { found: false, key: currentKey, node: this._findNodeByKey(currentKey) };
|
|
}
|
|
|
|
if (open && typeof this.loadChildren === 'function') {
|
|
const leafKey = currentKey;
|
|
const leafNode = this._findNodeByKey(leafKey);
|
|
if (leafNode && this._isExpandableNode(leafNode)) {
|
|
ensureExpanded(leafKey);
|
|
await this._loadChildrenForExpanded(leafKey, source, null, leafNode, leafKey.split('/'));
|
|
}
|
|
}
|
|
|
|
const finalNode = this._findNodeByKey(currentKey);
|
|
if (finalNode && select) {
|
|
ensureSelected(currentKey);
|
|
}
|
|
if (finalNode && scroll) {
|
|
await this.scrollToKey(currentKey);
|
|
}
|
|
|
|
externalSignal?.removeEventListener?.('abort', abortIfExternal);
|
|
return { found: Boolean(finalNode), key: currentKey, node: finalNode };
|
|
}
|
|
};
|