1 | import { toNodeId } from '../common';
|
2 | import { TreeIdentity as Identity } from '../TreeIdentity';
|
3 | export class TreeQuery {
|
4 | constructor(args) {
|
5 | this.walkDown = (visit) => {
|
6 | let stopped = false;
|
7 | const walk = (args) => {
|
8 | if (!args.node || stopped) {
|
9 | return;
|
10 | }
|
11 | const { id, key, namespace } = Identity.parse(args.node.id);
|
12 | if (this.namespace && namespace !== this.namespace) {
|
13 | return;
|
14 | }
|
15 | let skipChildren = false;
|
16 | visit({
|
17 | id,
|
18 | key,
|
19 | namespace,
|
20 | node: args.node,
|
21 | parent: args.parent,
|
22 | index: args.index,
|
23 | level: args.depth,
|
24 | stop: () => (stopped = true),
|
25 | skip: () => (skipChildren = true),
|
26 | });
|
27 | if (stopped) {
|
28 | return;
|
29 | }
|
30 | let index = -1;
|
31 | if (!skipChildren) {
|
32 | for (const child of TreeQuery.children(args.node, undefined, { assign: false })) {
|
33 | index++;
|
34 | walk({
|
35 | node: child,
|
36 | parent: args.node,
|
37 | index,
|
38 | depth: args.depth + 1,
|
39 | });
|
40 | }
|
41 | }
|
42 | };
|
43 | return walk({ node: this.root, depth: 0, index: -1 });
|
44 | };
|
45 | this.walkUp = (startAt, visit) => {
|
46 | let level = -1;
|
47 | const inner = (startAt, visit) => {
|
48 | const current = this.findById(toNodeId(startAt));
|
49 | level++;
|
50 | if (current) {
|
51 | let stop = false;
|
52 | const parentNode = this.parent(current);
|
53 | const { id, key, namespace } = Identity.parse(current.id);
|
54 | const args = {
|
55 | id,
|
56 | key,
|
57 | namespace,
|
58 | node: current,
|
59 | parent: parentNode,
|
60 | get index() {
|
61 | const id = current ? current.id : '';
|
62 | return !parentNode
|
63 | ? -1
|
64 | : (parentNode.children || []).findIndex((node) => node.id === id);
|
65 | },
|
66 | level,
|
67 | stop: () => (stop = true),
|
68 | };
|
69 | visit(args);
|
70 | if (!stop && parentNode) {
|
71 | inner(args.parent, visit);
|
72 | }
|
73 | }
|
74 | };
|
75 | return inner(startAt, visit);
|
76 | };
|
77 | this.find = (match) => {
|
78 | let result;
|
79 | this.walkDown((e) => {
|
80 | if (match(e) === true) {
|
81 | result = e.node;
|
82 | e.stop();
|
83 | }
|
84 | });
|
85 | return result ? result : undefined;
|
86 | };
|
87 | this.findById = (id) => {
|
88 | if (!id) {
|
89 | return undefined;
|
90 | }
|
91 | else {
|
92 | const target = Identity.parse(typeof id === 'string' ? id : id.id);
|
93 | return this.find((e) => {
|
94 | if (!target.namespace && e.key === target.key) {
|
95 | return true;
|
96 | }
|
97 | else {
|
98 | return e.key === target.key && e.namespace === target.namespace;
|
99 | }
|
100 | });
|
101 | }
|
102 | };
|
103 | this.parent = (node) => {
|
104 | if (!node) {
|
105 | return undefined;
|
106 | }
|
107 | if (typeof node !== 'object') {
|
108 | const id = node;
|
109 | node = this.findById(id);
|
110 | if (!node) {
|
111 | throw new Error(`Cannot find parent of '${id}' because that child node was not found.`);
|
112 | }
|
113 | }
|
114 | let result;
|
115 | const target = node;
|
116 | this.walkDown((e) => {
|
117 | if (TreeQuery.hasChild(e.node, target)) {
|
118 | result = e.node;
|
119 | e.stop();
|
120 | }
|
121 | });
|
122 | return result;
|
123 | };
|
124 | this.ancestor = (node, match) => {
|
125 | let result;
|
126 | this.walkUp(node, (e) => {
|
127 | if (match(e)) {
|
128 | result = e.node;
|
129 | e.stop();
|
130 | }
|
131 | });
|
132 | return result;
|
133 | };
|
134 | this.depth = (node) => {
|
135 | let depth = -1;
|
136 | if (!node || !this.root) {
|
137 | return depth;
|
138 | }
|
139 | else {
|
140 | const id = toNodeId(node);
|
141 | this.walkDown((e) => {
|
142 | if (e.node.id === id) {
|
143 | depth = e.level;
|
144 | e.stop();
|
145 | }
|
146 | });
|
147 | return depth;
|
148 | }
|
149 | };
|
150 | this.exists = (input) => {
|
151 | const node = typeof input === 'function' ? this.find(input) : this.findById(input);
|
152 | return Boolean(node);
|
153 | };
|
154 | this.root = args.root;
|
155 | this.namespace = (args.namespace || '').trim();
|
156 | }
|
157 | static create(args) {
|
158 | const input = args;
|
159 | const isQueryArgs = typeof input.root === 'object';
|
160 | const root = (isQueryArgs ? input.root : args);
|
161 | const namespace = isQueryArgs ? input.namespace || '' : '';
|
162 | return new TreeQuery({ root, namespace });
|
163 | }
|
164 | static children(of, fn, options = {}) {
|
165 | options = (typeof fn === 'object' ? fn : options) || {};
|
166 | const children = (!of ? [] : of.children || []);
|
167 | if (options.assign !== false && of && !of.children) {
|
168 | of.children = children;
|
169 | }
|
170 | if (typeof fn === 'function') {
|
171 | fn(children);
|
172 | }
|
173 | return children;
|
174 | }
|
175 | static hasChild(parent, child) {
|
176 | const nodes = TreeQuery.children(parent);
|
177 | const id = toNodeId(child);
|
178 | return nodes.some((node) => node.id === id);
|
179 | }
|
180 | static childAt(index, parent) {
|
181 | return TreeQuery.children(parent)[index];
|
182 | }
|
183 | }
|