Files
tp-tree-nav/mixins/tree-managed-state.js
2025-12-28 23:06:37 +01:00

145 lines
4.4 KiB
JavaScript

export const TreeManagedStateMixin = (superClass) => class TreeManagedStateMixin extends superClass {
_isExpandableNode(node) {
if (!node) return false;
if (Array.isArray(node.children) && node.children.length > 0) return true;
if (typeof this.loadChildren === 'function') {
if (typeof node.showAsExpandable === 'boolean') return node.showAsExpandable;
return true;
}
return false;
}
_findNodeByKey(key) {
let found = null;
const walk = (nodes, path) => {
if (!Array.isArray(nodes)) return;
nodes.forEach((node, index) => {
if (found) return;
const slug = node?.slug ?? `${index}`;
const nextPath = [...path, slug];
const nodeKey = nextPath.join('/');
if (nodeKey === key) {
found = node;
return;
}
if (Array.isArray(node?.children) && node.children.length > 0) {
walk(node.children, nextPath);
}
});
};
walk(this.roots, []);
return found;
}
getRootKey() {
const roots = Array.isArray(this.roots) ? this.roots : [];
if (!roots.length) return '';
const first = roots[0];
const slug = first?.slug ?? '0';
return String(slug);
}
_updateNodeStatesByKey(key, updater) {
const map = (nodes, path = []) => {
if (!Array.isArray(nodes)) return nodes;
return nodes.map((node, index) => {
const slug = node?.slug ?? `${index}`;
const nextPath = [...path, slug];
const nodeKey = nextPath.join('/');
let nextNode = node;
if (nodeKey === key) {
nextNode = updater(node) ?? node;
}
const children = Array.isArray(nextNode?.children)
? map(nextNode.children, nextPath)
: nextNode?.children;
if (children !== nextNode?.children) {
return { ...nextNode, children };
}
return nextNode;
});
};
this.roots = map(this.roots);
}
_applySelectionStates() {
const selected = new Set(this._selectedPathSet);
const selectionState = this.selectionState;
const { nodes } = this._mapTree(this.roots, (node, path) => {
const key = Array.isArray(path) ? path.join('/') : '';
const states = Array.isArray(node?.states) ? node.states : [];
const without = states.filter((s) => s !== selectionState);
if (selected.has(key)) {
if (!without.includes(selectionState)) without.push(selectionState);
return { ...node, states: without };
}
if (without.length !== states.length) {
return { ...node, states: without };
}
return node;
});
this.roots = nodes;
}
_rebuildManagedTree() {
this._expandedPathSet = new Set(this.expandedPaths || this._expandedPathSet);
this._selectedPathSet = new Set(this.selectedPaths || this._selectedPathSet);
const { nodes, allPaths, expandedPaths, selectedPaths } = this.constructor.buildTree(
Array.isArray(this.items) ? this.items : [],
{
expandedPaths: this._expandedPathSet,
selectedPaths: this._selectedPathSet,
selectionState: this.selectionState,
applyStates: this.applyStates,
knownPaths: this._knownPaths,
autoExpandNew: this.autoExpandNew,
}
);
this._knownPaths = allPaths;
this._expandedPathSet = new Set([...expandedPaths].filter((p) => allPaths.has(p)));
this._selectedPathSet = new Set([...selectedPaths].filter((p) => allPaths.has(p)));
this.roots = nodes;
}
getNodesWithState(state) {
const matches = [];
this._walkNodes(this.roots, [], (node, path) => {
if (Array.isArray(node?.states) && node.states.includes(state)) {
matches.push({ node, path });
}
});
return matches;
}
clearState(state) {
const { nodes } = this._mapTree(this.roots, (node) => {
const states = Array.isArray(node?.states)
? node.states.filter((s) => s !== state)
: [];
const changed = (node.states?.length ?? 0) !== states.length;
return changed ? { ...node, states } : node;
});
return nodes;
}
applyStateIf(state, predicate) {
if (typeof predicate !== 'function') return this.roots;
const { nodes } = this._mapTree(this.roots, (node, path) => {
const states = Array.isArray(node?.states) ? [...node.states] : [];
if (predicate(node, path) && !states.includes(state)) {
states.push(state);
return { ...node, states };
}
return node;
});
return nodes;
}
};