1/*
2 * RapidContext <https://www.rapidcontext.com/>
3 * Copyright (c) 2007-2025 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 widget.
23 *
24 * @constructor
25 * @param {Object} attrs the widget and node attributes
26 * @param {boolean} [attrs.hidden] the hidden widget flag, defaults to false
27 * @param {...TreeNode} [child] the child tree node widgets
28 *
29 * @return {Widget} the widget DOM node
30 *
31 * @class The tree widget class. Used to provide a dynamic tree with expandable
32 * tree nodes, using a number of `<div>` HTML elements.
33 * @extends RapidContext.Widget
34 *
35 * @example <caption>JavaScript</caption>
36 * var tree = RapidContext.Widget.Tree({ style: { width: "200px", height: "400px" } });
37 * var root = RapidContext.Widget.TreeNode({ folder: true, name: "Root" });
38 * var child = RapidContext.Widget.TreeNode({ name: "Child" });
39 * root.addAll(child);
40 * tree.addAll(root);
41 *
42 * @example <caption>User Interface XML</caption>
43 * <Tree style="width: 200px; height: 400px;">
44 * <TreeNode name="Root">
45 * <TreeNode name="Child" />
46 * </TreeNode>
47 * </Tree>
48 */
49RapidContext.Widget.Tree = function (attrs/*, ...*/) {
50 const o = RapidContext.UI.DIV(attrs);
51 RapidContext.Widget._widgetMixin(o, RapidContext.Widget.Tree);
52 o.classList.add("widgetTree");
53 o.selectedPath = null;
54 o.setAttrs(attrs);
55 o.addAll(Array.from(arguments).slice(1));
56 o.on("click", ".widgetTreeNodeLabel", o._handleNodeLabelClick);
57 return o;
58};
59
60// Register widget class
61RapidContext.Widget.Classes.Tree = RapidContext.Widget.Tree;
62
63/**
64 * Emitted when a tree node is expanded.
65 *
66 * @name RapidContext.Widget.Tree#onexpand
67 * @event
68 */
69
70/**
71 * Emitted when a tree node is collapsed.
72 *
73 * @name RapidContext.Widget.Tree#oncollapse
74 * @event
75 */
76
77/**
78 * Emitted when a tree node is selected. It will be emitted after
79 * "onunselect" if another node was previously selected.
80 *
81 * @name RapidContext.Widget.Tree#onselect
82 * @event
83 */
84
85/**
86 * Emitted when a tree node selection is removed. It will be emitted
87 * before "onselect" if another node was previously selected.
88 *
89 * @name RapidContext.Widget.Tree#onunselect
90 * @event
91 */
92
93/**
94 * Handles node label click events.
95 *
96 * @param {Event} evt the DOM Event object
97 */
98RapidContext.Widget.Tree.prototype._handleNodeLabelClick = function (evt) {
99 const node = evt.target.closest("div.widgetTreeNode");
100 if (RapidContext.Widget.isWidget(evt.target, "Icon")) {
101 node.toggle();
102 } else {
103 node.select();
104 }
105};
106
107/**
108 * Adds a single child tree node widget to this widget.
109 *
110 * @param {Widget} child the tree node widget to add
111 */
112RapidContext.Widget.Tree.prototype.addChildNode = function (child) {
113 if (!RapidContext.Widget.isWidget(child, "TreeNode")) {
114 throw new Error("Tree widget can only have TreeNode children");
115 }
116 this.append(child);
117};
118
119/**
120 * Removes all marked tree nodes. When adding or updating tree nodes, any
121 * node modified is automatically unmarked (e.g. by calling `setAttrs` on the
122 * tree nodes or `addPath` on the tree). This makes it easy to prune a tree
123 * after an update, by initially marking all tree nodes with `markAll()`,
124 * inserting or touching all nodes to keep, and finally calling this method to
125 * remove the remaining nodes.
126 *
127 * @example
128 * tree.markAll();
129 * tree.addPath(["Root", "Child"]);
130 * ...
131 * tree.removeAllMarked();
132 */
133RapidContext.Widget.Tree.prototype.removeAllMarked = function () {
134 const children = this.getChildNodes();
135 for (let i = 0; i < children.length; i++) {
136 if (children[i].marked === true) {
137 this.removeChildNode(children[i]);
138 } else {
139 children[i].removeAllMarked();
140 }
141 }
142};
143
144/**
145 * Marks this tree node and all child nodes recursively. When adding or
146 * updating tree nodes, any node modified is automatically unmarked (e.g. by
147 * calling `setAttrs` on the tree nodes or `addPath` on the tree). This makes
148 * it easy to prune a tree after an update, by initially marking all tree
149 * nodes, inserting or touching all nodes to keep, and finally calling
150 * `removeAllMarked()` to remove the remaining nodes.
151 *
152 * @example
153 * tree.markAll();
154 * tree.addPath(["Root", "Child"]);
155 * ...
156 * tree.removeAllMarked();
157 */
158RapidContext.Widget.Tree.prototype.markAll = function () {
159 const children = this.getChildNodes();
160 for (let i = 0; i < children.length; i++) {
161 children[i].markAll();
162 }
163};
164
165/**
166 * Finds a root tree node with the specified name.
167 *
168 * @param {string} name the root tree node name
169 *
170 * @return {Widget} the root tree node found, or
171 * null if not found
172 */
173RapidContext.Widget.Tree.prototype.findRoot = function (name) {
174 const children = this.getChildNodes();
175 for (let i = 0; i < children.length; i++) {
176 if (children[i].name == name) {
177 return children[i];
178 }
179 }
180 return null;
181};
182
183/**
184 * Searches for a tree node from the specified path.
185 *
186 * @param {Array} path the tree node path (array of names)
187 *
188 * @return {Widget} the descendant tree node found, or
189 * null if not found
190 */
191RapidContext.Widget.Tree.prototype.findByPath = function (path) {
192 if (path == null || path.length < 1) {
193 return null;
194 }
195 const root = this.findRoot(path[0]);
196 if (root != null) {
197 return root.findByPath(path.slice(1));
198 } else {
199 return null;
200 }
201};
202
203/**
204 * Returns the currently selected tree node.
205 *
206 * @return {Widget} the currently selected tree node, or
207 * null if no node is selected
208 */
209RapidContext.Widget.Tree.prototype.selectedChild = function () {
210 return this.findByPath(this.selectedPath);
211};
212
213/**
214 * Sets the currently selected node in the tree. This method is only
215 * called from the tree node `select()` and `unselect()` methods.
216 *
217 * @param {Widget} node the new selected tree node, or null for none
218 */
219RapidContext.Widget.Tree.prototype._handleSelect = function (node) {
220 const prev = this.selectedChild();
221 if (node == null) {
222 this.selectedPath = null;
223 this.emit("unselect");
224 } else {
225 if (prev != null && prev !== node) {
226 prev.unselect();
227 }
228 this.selectedPath = node.path();
229 this.emit("select");
230 }
231};
232
233/**
234 * Recursively expands all nodes. If a depth is specified,
235 * expansions will not continue below that depth.
236 *
237 * @param {number} [depth] the optional maximum depth
238 */
239RapidContext.Widget.Tree.prototype.expandAll = function (depth) {
240 if (typeof(depth) !== "number") {
241 depth = 10;
242 }
243 if (depth > 0) {
244 const children = this.getChildNodes();
245 for (let i = 0; i < children.length; i++) {
246 children[i].expandAll(depth - 1);
247 }
248 }
249};
250
251/**
252 * Recursively collapses all nodes. If a depth is specified, only
253 * nodes below that depth will be collapsed.
254 *
255 * @param {number} [depth] the optional minimum depth
256 */
257RapidContext.Widget.Tree.prototype.collapseAll = function (depth) {
258 if (typeof(depth) !== "number") {
259 depth = 0;
260 }
261 const children = this.getChildNodes();
262 for (let i = 0; i < children.length; i++) {
263 children[i].collapseAll(depth - 1);
264 }
265};
266
267/**
268 * Adds a path to the tree as a recursive list of child nodes. If
269 * nodes in the specified path already exists, they will be used
270 * instead of creating new nodes.
271 *
272 * @param {Array} path the tree node path (array of names)
273 *
274 * @return {Widget} the last node in the path
275 */
276RapidContext.Widget.Tree.prototype.addPath = function (path) {
277 if (path == null || path.length < 1) {
278 return null;
279 }
280 let node = this.findRoot(path[0]);
281 if (node == null) {
282 node = RapidContext.Widget.TreeNode({ name: path[0], folder: (path.length > 1) });
283 this.addChildNode(node);
284 }
285 node.marked = false;
286 for (let i = 1; i < path.length; i++) {
287 let child = node.findChild(path[i]);
288 if (child == null) {
289 const attrs = { name: path[i], folder: (path.length > i + 1) };
290 child = RapidContext.Widget.TreeNode(attrs);
291 node.addChildNode(child);
292 }
293 child.marked = false;
294 node = child;
295 }
296 return node;
297};
298
RapidContext
Access · Discovery · Insight
www.rapidcontext.com