1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2023 Per Cederberg. All rights reserved.
4 *
5 * This program is free software: you can redistribute it and/or
6 * modify it under the terms of the BSD license.
7 *
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11 *
12 * See the RapidContext LICENSE for more details.
13 */
14
15// Namespace initialization
16if (typeof(RapidContext) == "undefined") {
17 RapidContext = {};
18}
19RapidContext.Widget = RapidContext.Widget || { Classes: {} };
20
21/**
22 * Creates a new tree node widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {string} attrs.name the tree node name
27 * @param {boolean} [attrs.folder] the folder flag, defaults to `false` if no
28 * child nodes are provided in constructor call
29 * @param {string} [attrs.icon] the icon reference to use, defaults
30 * to "FOLDER" for folders and "DOCUMENT" otherwise
31 * @param {string} [attrs.tooltip] the tooltip text when hovering
32 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to `false`
33 * @param {...TreeNode} [child] the child tree node widgets
34 *
35 * @return {Widget} the widget DOM node
36 *
37 * @class The tree node widget class. Used to provide a tree node in a tree,
38 * using a number of `<div>` HTML elements. Note that events should
39 * normally not be listened for on individual tree nodes, but rather on
40 * the tree as a whole.
41 * @extends RapidContext.Widget
42 *
43 * @example <caption>JavaScript</caption>
44 * let parent = RapidContext.Widget.TreeNode({ folder: true, name: "Parent" });
45 * let child = RapidContext.Widget.TreeNode({ name: "Child" });
46 * parent.addAll(child);
47 *
48 * @example <caption>User Interface XML</caption>
49 * <TreeNode name="Parent">
50 * <TreeNode name="Child" />
51 * </TreeNode>
52 */
53RapidContext.Widget.TreeNode = function (attrs/*, ...*/) {
54 let toggle = RapidContext.Widget.Icon("fa fa-fw");
55 let icon = RapidContext.Widget.Icon("fa fa-fw fa-dot-circle-o");
56 let label = MochiKit.DOM.SPAN({ "class": "widgetTreeNodeText" });
57 let cls = "widgetTreeNodeLabel overflow-ellipsis text-nowrap";
58 let div = MochiKit.DOM.DIV({ "class": cls }, toggle, icon, label);
59 let o = MochiKit.DOM.DIV({}, div);
60 RapidContext.Widget._widgetMixin(o, RapidContext.Widget.TreeNode);
61 o.classList.add("widgetTreeNode");
62 let isFolder = (arguments.length > 1);
63 attrs = Object.assign({ name: "Tree Node", folder: isFolder }, attrs);
64 o.setAttrs(attrs);
65 o.addAll(Array.from(arguments).slice(1));
66 return o;
67};
68
69// Register widget class
70RapidContext.Widget.Classes.TreeNode = RapidContext.Widget.TreeNode;
71
72/**
73 * Returns and optionally creates the widget container DOM node. If a
74 * child container is created, it will be hidden by default.
75 *
76 * @param {boolean} [create] the create flag, defaults to `false`
77 *
78 * @return {Node} the container DOM node, or
79 * null if this widget has no container (yet)
80 */
81RapidContext.Widget.TreeNode.prototype._containerNode = function (create) {
82 let container = this.lastChild;
83 if (container.classList.contains("widgetTreeNodeContainer")) {
84 return container;
85 } else if (create) {
86 container = MochiKit.DOM.DIV({ "class": "widgetTreeNodeContainer widgetHidden" });
87 this.append(container);
88 this.firstChild.childNodes[0].setAttrs({ "class": "fa fa-fw fa-plus-square-o" });
89 if (!this.icon) {
90 this.firstChild.childNodes[1].setAttrs({ "class": "fa fa-fw fa-folder" });
91 }
92 return container;
93 } else {
94 return null;
95 }
96};
97
98/**
99 * Updates the widget or HTML DOM node attributes.
100 *
101 * @param {Object} attrs the widget and node attributes to set
102 * @param {string} [attrs.name] the tree node name
103 * @param {boolean} [attrs.folder] the folder flag, cannot be
104 * reverted to `false` once set (implicitly or explicitly)
105 * @param {Icon|Object|string} [attrs.icon] icon the icon to set, or
106 * null to remove
107 * @param {string} [attrs.tooltip] the tooltip text when hovering
108 * @param {boolean} [attrs.hidden] the hidden widget flag
109 */
110RapidContext.Widget.TreeNode.prototype.setAttrs = function (attrs) {
111 this.marked = false;
112 attrs = Object.assign({}, attrs);
113 if ("name" in attrs) {
114 this.querySelector(".widgetTreeNodeText").innerText = attrs.name;
115 }
116 if ("folder" in attrs) {
117 this._containerNode(RapidContext.Data.bool(attrs.folder));
118 delete attrs.folder;
119 }
120 if ("icon" in attrs) {
121 let icon = RapidContext.Widget.Icon(attrs.icon);
122 this.firstChild.childNodes[1].replaceWith(icon);
123 }
124 if ("tooltip" in attrs) {
125 this.firstChild.title = attrs.tooltip;
126 }
127 this.__setAttrs(attrs);
128};
129
130/**
131 * Adds a single child tree node widget to this widget.
132 *
133 * @param {Widget} child the tree node widget to add
134 */
135RapidContext.Widget.TreeNode.prototype.addChildNode = function (child) {
136 if (!RapidContext.Widget.isWidget(child, "TreeNode")) {
137 throw new Error("TreeNode widget can only have TreeNode children");
138 }
139 this._containerNode(true).append(child);
140};
141
142/**
143 * Removes all marked tree nodes. When adding or updating tree nodes, any
144 * node modified is automatically unmarked (e.g. by calling `setAttrs`). This
145 * makes it easy to prune a tree after an update, by initially marking all
146 * tree nodes with `markAll()`, inserting or touching all nodes to keep, and
147 * finally calling this method to remove the remaining nodes.
148 *
149 * @example
150 * parent.markAll();
151 * parent.setAttrs();
152 * child.setAttrs();
153 * ...
154 * parent.removeAllMarked();
155 */
156RapidContext.Widget.TreeNode.prototype.removeAllMarked = function () {
157 let children = this.getChildNodes();
158 for (let i = 0; i < children.length; i++) {
159 if (children[i].marked === true) {
160 this.removeChildNode(children[i]);
161 } else {
162 children[i].removeAllMarked();
163 }
164 }
165};
166
167/**
168 * Marks this tree node and all child nodes recursively. When adding or
169 * updating tree nodes, any node modified is automatically unmarked (e.g. by
170 * calling `setAttrs`). This makes it easy to prune a tree after an update, by
171 * initially marking all tree nodes, inserting or touching all nodes to keep,
172 * and finally calling `removeAllMarked()` to remove the remaining nodes.
173 *
174 * @example
175 * parent.markAll();
176 * parent.setAttrs();
177 * child.setAttrs();
178 * ...
179 * parent.removeAllMarked();
180 */
181RapidContext.Widget.TreeNode.prototype.markAll = function () {
182 this.marked = true;
183 let children = this.getChildNodes();
184 for (let i = 0; i < children.length; i++) {
185 children[i].markAll();
186 }
187};
188
189/**
190 * Checks if this node is a folder.
191 *
192 * @return {boolean} `true` if this node is a folder, or
193 * `false` otherwise
194 */
195RapidContext.Widget.TreeNode.prototype.isFolder = function () {
196 return this._containerNode() != null;
197};
198
199/**
200 * Checks if this folder node is expanded.
201 *
202 * @return {boolean} `true` if this node is expanded, or
203 * `false` otherwise
204 */
205RapidContext.Widget.TreeNode.prototype.isExpanded = function () {
206 let container = this._containerNode();
207 return !!container && !container.classList.contains("widgetHidden");
208};
209
210/**
211 * Checks if this node is selected.
212 *
213 * @return {boolean} `true` if the node is selected, or
214 * `false` otherwise
215 */
216RapidContext.Widget.TreeNode.prototype.isSelected = function () {
217 return this.firstChild.classList.contains("selected");
218};
219
220/**
221 * Returns the ancestor tree widget.
222 *
223 * @return {Widget} the ancestor tree widget, or
224 * null if none was found
225 */
226RapidContext.Widget.TreeNode.prototype.tree = function () {
227 let parent = this.parent();
228 if (parent != null) {
229 return parent.tree();
230 }
231 if (RapidContext.Widget.isWidget(this.parentNode, "Tree")) {
232 return this.parentNode;
233 } else {
234 return null;
235 }
236};
237
238/**
239 * Returns the parent tree node widget.
240 *
241 * @return {Widget} the parent tree node widget, or
242 * null if this is a root node
243 */
244RapidContext.Widget.TreeNode.prototype.parent = function () {
245 let node = this.parentNode;
246 if (node && node.classList.contains("widgetTreeNodeContainer")) {
247 return node.parentNode;
248 } else {
249 return null;
250 }
251};
252
253/**
254 * Returns the path to this tree node.
255 *
256 * @return {Array} the tree node path, i.e an array of node names
257 */
258RapidContext.Widget.TreeNode.prototype.path = function () {
259 let parent = this.parent();
260 if (parent == null) {
261 return [this.name];
262 } else {
263 let path = parent.path();
264 path.push(this.name);
265 return path;
266 }
267};
268
269/**
270 * Finds a child tree node with the specified name.
271 *
272 * @param {string} name the child tree node name
273 *
274 * @return {Widget} the child tree node found, or
275 * null if not found
276 */
277RapidContext.Widget.TreeNode.prototype.findChild = function (name) {
278 let children = this.getChildNodes();
279 for (let i = 0; i < children.length; i++) {
280 if (children[i].name == name) {
281 return children[i];
282 }
283 }
284 return null;
285};
286
287/**
288 * Searches for a descendant tree node from the specified path.
289 *
290 * @param {Array} path the tree node path (array of node names)
291 *
292 * @return {Widget} the descendant tree node found, or
293 * null if not found
294 */
295RapidContext.Widget.TreeNode.prototype.findByPath = function (path) {
296 let node = this;
297 for (let i = 0; node != null && path != null && i < path.length; i++) {
298 node = node.findChild(path[i]);
299 }
300 return node;
301};
302
303/**
304 * Selects this tree node.
305 */
306RapidContext.Widget.TreeNode.prototype.select = function () {
307 this.firstChild.classList.add("selected");
308 let tree = this.tree();
309 if (tree != null) {
310 tree._handleSelect(this);
311 }
312 this.expand();
313};
314
315/**
316 * Unselects this tree node.
317 */
318RapidContext.Widget.TreeNode.prototype.unselect = function () {
319 if (this.isSelected()) {
320 this.firstChild.classList.remove("selected");
321 let tree = this.tree();
322 if (tree != null) {
323 tree._handleSelect(null);
324 }
325 }
326};
327
328/**
329 * Expands this node to display any child nodes. If the parent node
330 * is not expanded, it will be expanded as well.
331 */
332RapidContext.Widget.TreeNode.prototype.expand = function () {
333 let parent = this.parent();
334 if (parent != null && !parent.isExpanded()) {
335 parent.expand();
336 }
337 let container = this._containerNode();
338 if (container != null && !this.isExpanded()) {
339 this.firstChild.childNodes[0].setAttrs({ "class": "fa fa-fw fa-minus-square-o" });
340 if (!this.icon) {
341 this.firstChild.childNodes[1].setAttrs({ "class": "fa fa-fw fa-folder-open" });
342 }
343 container.classList.remove("widgetHidden");
344 let tree = this.tree();
345 if (tree != null) {
346 let detail = { tree: tree, node: this };
347 tree.emit("expand", { detail: detail });
348 }
349 }
350};
351
352/**
353 * Recursively expands this node and all its children. If a depth is
354 * specified, expansions will not continue below that depth.
355 *
356 * @param {number} [depth] the optional maximum depth
357 */
358RapidContext.Widget.TreeNode.prototype.expandAll = function (depth) {
359 if (typeof(depth) !== "number") {
360 depth = 10;
361 }
362 this.expand();
363 let children = this.getChildNodes();
364 for (let i = 0; depth > 0 && i < children.length; i++) {
365 children[i].expandAll(depth - 1);
366 }
367};
368
369/**
370 * Collapses this node to hide any child nodes.
371 */
372RapidContext.Widget.TreeNode.prototype.collapse = function () {
373 let container = this._containerNode();
374 if (container != null && this.isExpanded()) {
375 this.firstChild.childNodes[0].setAttrs({ "class": "fa fa-fw fa-plus-square-o" });
376 if (!this.icon) {
377 this.firstChild.childNodes[1].setAttrs({ "class": "fa fa-fw fa-folder" });
378 }
379 container.classList.add("widgetHidden");
380 let tree = this.tree();
381 if (tree != null) {
382 let detail = { tree: tree, node: this };
383 tree.emit("collapse", { detail: detail });
384 }
385 }
386};
387
388/**
389 * Recursively collapses this node and all its children. If a depth
390 * is specified, only children below that depth will be collapsed.
391 *
392 * @param {number} [depth] the optional minimum depth
393 */
394RapidContext.Widget.TreeNode.prototype.collapseAll = function (depth) {
395 if (typeof(depth) !== "number") {
396 depth = 0;
397 }
398 if (depth <= 0) {
399 this.collapse();
400 }
401 let children = this.getChildNodes();
402 for (let i = 0; i < children.length; i++) {
403 children[i].collapseAll(depth - 1);
404 }
405};
406
407/**
408 * Toggles expand and collapse for this node.
409 */
410RapidContext.Widget.TreeNode.prototype.toggle = function () {
411 if (this.isExpanded()) {
412 this.collapse();
413 } else {
414 this.expand();
415 }
416};
417
RapidContext
Access · Discovery · Insight
www.rapidcontext.com