@platform/state
Version:
A small, simple, strongly typed, [rx/observable] state-machine.
184 lines (183 loc) • 6.4 kB
JavaScript
import { toNodeId } from '../common';
import { TreeIdentity as Identity } from '../TreeIdentity';
export class TreeQuery {
constructor(args) {
this.walkDown = (visit) => {
let stopped = false;
const walk = (args) => {
if (!args.node || stopped) {
return;
}
const { id, key, namespace } = Identity.parse(args.node.id);
if (this.namespace && namespace !== this.namespace) {
return;
}
let skipChildren = false;
visit({
id,
key,
namespace,
node: args.node,
parent: args.parent,
index: args.index,
level: args.depth,
stop: () => (stopped = true),
skip: () => (skipChildren = true),
});
if (stopped) {
return;
}
let index = -1;
if (!skipChildren) {
for (const child of TreeQuery.children(args.node, undefined, { assign: false })) {
index++;
walk({
node: child,
parent: args.node,
index,
depth: args.depth + 1,
});
}
}
};
return walk({ node: this.root, depth: 0, index: -1 });
};
this.walkUp = (startAt, visit) => {
let level = -1;
const inner = (startAt, visit) => {
const current = this.findById(toNodeId(startAt));
level++;
if (current) {
let stop = false;
const parentNode = this.parent(current);
const { id, key, namespace } = Identity.parse(current.id);
const args = {
id,
key,
namespace,
node: current,
parent: parentNode,
get index() {
const id = current ? current.id : '';
return !parentNode
? -1
: (parentNode.children || []).findIndex((node) => node.id === id);
},
level,
stop: () => (stop = true),
};
visit(args);
if (!stop && parentNode) {
inner(args.parent, visit);
}
}
};
return inner(startAt, visit);
};
this.find = (match) => {
let result;
this.walkDown((e) => {
if (match(e) === true) {
result = e.node;
e.stop();
}
});
return result ? result : undefined;
};
this.findById = (id) => {
if (!id) {
return undefined;
}
else {
const target = Identity.parse(typeof id === 'string' ? id : id.id);
return this.find((e) => {
if (!target.namespace && e.key === target.key) {
return true;
}
else {
return e.key === target.key && e.namespace === target.namespace;
}
});
}
};
this.parent = (node) => {
if (!node) {
return undefined;
}
if (typeof node !== 'object') {
const id = node;
node = this.findById(id);
if (!node) {
throw new Error(`Cannot find parent of '${id}' because that child node was not found.`);
}
}
let result;
const target = node;
this.walkDown((e) => {
if (TreeQuery.hasChild(e.node, target)) {
result = e.node;
e.stop();
}
});
return result;
};
this.ancestor = (node, match) => {
let result;
this.walkUp(node, (e) => {
if (match(e)) {
result = e.node;
e.stop();
}
});
return result;
};
this.depth = (node) => {
let depth = -1;
if (!node || !this.root) {
return depth;
}
else {
const id = toNodeId(node);
this.walkDown((e) => {
if (e.node.id === id) {
depth = e.level;
e.stop();
}
});
return depth;
}
};
this.exists = (input) => {
const node = typeof input === 'function' ? this.find(input) : this.findById(input);
return Boolean(node);
};
this.root = args.root;
this.namespace = (args.namespace || '').trim();
}
static create(args) {
const input = args;
const isQueryArgs = typeof input.root === 'object';
const root = (isQueryArgs ? input.root : args);
const namespace = isQueryArgs ? input.namespace || '' : '';
return new TreeQuery({ root, namespace });
}
static children(of, fn, options = {}) {
options = (typeof fn === 'object' ? fn : options) || {};
const children = (!of ? [] : of.children || []);
if (options.assign !== false && of && !of.children) {
of.children = children;
}
if (typeof fn === 'function') {
fn(children);
}
return children;
}
static hasChild(parent, child) {
const nodes = TreeQuery.children(parent);
const id = toNodeId(child);
return nodes.some((node) => node.id === id);
}
static childAt(index, parent) {
return TreeQuery.children(parent)[index];
}
}