1/*!
2 * jquery.fancytree.table.js
3 *
4 * Render tree as table (aka 'tree grid', 'table tree').
5 * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
6 *
7 * Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
8 *
9 * Released under the MIT license
10 * https://github.com/mar10/fancytree/wiki/LicenseInfo
11 *
12 * @version 2.38.3
13 * @date 2023-02-01T20:52:50Z
14 */
15
16(function (factory) {
17	if (typeof define === "function" && define.amd) {
18		// AMD. Register as an anonymous module.
19		define(["jquery", "./jquery.fancytree"], factory);
20	} else if (typeof module === "object" && module.exports) {
21		// Node/CommonJS
22		require("./jquery.fancytree");
23		module.exports = factory(require("jquery"));
24	} else {
25		// Browser globals
26		factory(jQuery);
27	}
28})(function ($) {
29	"use strict";
30
31	/******************************************************************************
32	 * Private functions and variables
33	 */
34	var _assert = $.ui.fancytree.assert;
35
36	function insertFirstChild(referenceNode, newNode) {
37		referenceNode.insertBefore(newNode, referenceNode.firstChild);
38	}
39
40	function insertSiblingAfter(referenceNode, newNode) {
41		referenceNode.parentNode.insertBefore(
42			newNode,
43			referenceNode.nextSibling
44		);
45	}
46
47	/* Show/hide all rows that are structural descendants of `parent`. */
48	function setChildRowVisibility(parent, flag) {
49		parent.visit(function (node) {
50			var tr = node.tr;
51			// currentFlag = node.hide ? false : flag; // fix for ext-filter
52			if (tr) {
53				tr.style.display = node.hide || !flag ? "none" : "";
54			}
55			if (!node.expanded) {
56				return "skip";
57			}
58		});
59	}
60
61	/* Find node that is rendered in previous row. */
62	function findPrevRowNode(node) {
63		var i,
64			last,
65			prev,
66			parent = node.parent,
67			siblings = parent ? parent.children : null;
68
69		if (siblings && siblings.length > 1 && siblings[0] !== node) {
70			// use the lowest descendant of the preceeding sibling
71			i = $.inArray(node, siblings);
72			prev = siblings[i - 1];
73			_assert(prev.tr);
74			// descend to lowest child (with a <tr> tag)
75			while (prev.children && prev.children.length) {
76				last = prev.children[prev.children.length - 1];
77				if (!last.tr) {
78					break;
79				}
80				prev = last;
81			}
82		} else {
83			// if there is no preceding sibling, use the direct parent
84			prev = parent;
85		}
86		return prev;
87	}
88
89	$.ui.fancytree.registerExtension({
90		name: "table",
91		version: "2.38.3",
92		// Default options for this extension.
93		options: {
94			checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
95			indentation: 16, // indent every node level by 16px
96			mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR
97			nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0)
98		},
99		// Overide virtual methods for this extension.
100		// `this`       : is this extension object
101		// `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
102		treeInit: function (ctx) {
103			var i,
104				n,
105				$row,
106				$tbody,
107				tree = ctx.tree,
108				opts = ctx.options,
109				tableOpts = opts.table,
110				$table = tree.widget.element;
111
112			if (tableOpts.customStatus != null) {
113				if (opts.renderStatusColumns == null) {
114					tree.warn(
115						"The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead."
116					);
117					opts.renderStatusColumns = tableOpts.customStatus;
118				} else {
119					$.error(
120						"The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead."
121					);
122				}
123			}
124			if (opts.renderStatusColumns) {
125				if (opts.renderStatusColumns === true) {
126					opts.renderStatusColumns = opts.renderColumns;
127					// } else if( opts.renderStatusColumns === "wide" ) {
128					// 	opts.renderStatusColumns = _renderStatusNodeWide;
129				}
130			}
131
132			$table.addClass("fancytree-container fancytree-ext-table");
133			$tbody = $table.find(">tbody");
134			if (!$tbody.length) {
135				// TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s:
136				if ($table.find(">tr").length) {
137					$.error(
138						"Expected table > tbody > tr. If you see this please open an issue."
139					);
140				}
141				$tbody = $("<tbody>").appendTo($table);
142			}
143
144			tree.tbody = $tbody[0];
145
146			// Prepare row templates:
147			// Determine column count from table header if any
148			tree.columnCount = $("thead >tr", $table)
149				.last()
150				.find(">th", $table).length;
151			// Read TR templates from tbody if any
152			$row = $tbody.children("tr").first();
153			if ($row.length) {
154				n = $row.children("td").length;
155				if (tree.columnCount && n !== tree.columnCount) {
156					tree.warn(
157						"Column count mismatch between thead (" +
158							tree.columnCount +
159							") and tbody (" +
160							n +
161							"): using tbody."
162					);
163					tree.columnCount = n;
164				}
165				$row = $row.clone();
166			} else {
167				// Only thead is defined: create default row markup
168				_assert(
169					tree.columnCount >= 1,
170					"Need either <thead> or <tbody> with <td> elements to determine column count."
171				);
172				$row = $("<tr />");
173				for (i = 0; i < tree.columnCount; i++) {
174					$row.append("<td />");
175				}
176			}
177			$row.find(">td")
178				.eq(tableOpts.nodeColumnIdx)
179				.html("<span class='fancytree-node' />");
180			if (opts.aria) {
181				$row.attr("role", "row");
182				$row.find("td").attr("role", "gridcell");
183			}
184			tree.rowFragment = document.createDocumentFragment();
185			tree.rowFragment.appendChild($row.get(0));
186
187			// // If tbody contains a second row, use this as status node template
188			// $row = $tbody.children("tr").eq(1);
189			// if( $row.length === 0 ) {
190			// 	tree.statusRowFragment = tree.rowFragment;
191			// } else {
192			// 	$row = $row.clone();
193			// 	tree.statusRowFragment = document.createDocumentFragment();
194			// 	tree.statusRowFragment.appendChild($row.get(0));
195			// }
196			//
197			$tbody.empty();
198
199			// Make sure that status classes are set on the node's <tr> elements
200			tree.statusClassPropName = "tr";
201			tree.ariaPropName = "tr";
202			this.nodeContainerAttrName = "tr";
203
204			// #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-table
205			tree.$container = $table;
206
207			this._superApply(arguments);
208
209			// standard Fancytree created a root UL
210			$(tree.rootNode.ul).remove();
211			tree.rootNode.ul = null;
212
213			// Add container to the TAB chain
214			// #577: Allow to set tabindex to "0", "-1" and ""
215			this.$container.attr("tabindex", opts.tabindex);
216			// this.$container.attr("tabindex", opts.tabbable ? "0" : "-1");
217			if (opts.aria) {
218				tree.$container
219					.attr("role", "treegrid")
220					.attr("aria-readonly", true);
221			}
222		},
223		nodeRemoveChildMarkup: function (ctx) {
224			var node = ctx.node;
225			//		node.debug("nodeRemoveChildMarkup()");
226			node.visit(function (n) {
227				if (n.tr) {
228					$(n.tr).remove();
229					n.tr = null;
230				}
231			});
232		},
233		nodeRemoveMarkup: function (ctx) {
234			var node = ctx.node;
235			//		node.debug("nodeRemoveMarkup()");
236			if (node.tr) {
237				$(node.tr).remove();
238				node.tr = null;
239			}
240			this.nodeRemoveChildMarkup(ctx);
241		},
242		/* Override standard render. */
243		nodeRender: function (ctx, force, deep, collapsed, _recursive) {
244			var children,
245				firstTr,
246				i,
247				l,
248				newRow,
249				prevNode,
250				prevTr,
251				subCtx,
252				tree = ctx.tree,
253				node = ctx.node,
254				opts = ctx.options,
255				isRootNode = !node.parent;
256
257			if (tree._enableUpdate === false) {
258				// $.ui.fancytree.debug("*** nodeRender _enableUpdate: false");
259				return;
260			}
261			if (!_recursive) {
262				ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
263			}
264			// $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
265			if (!isRootNode) {
266				if (node.tr && force) {
267					this.nodeRemoveMarkup(ctx);
268				}
269				if (node.tr) {
270					if (force) {
271						// Set icon, link, and title (normally this is only required on initial render)
272						this.nodeRenderTitle(ctx); // triggers renderColumns()
273					} else {
274						// Update element classes according to node state
275						this.nodeRenderStatus(ctx);
276					}
277				} else {
278					if (ctx.hasCollapsedParents && !deep) {
279						// #166: we assume that the parent will be (recursively) rendered
280						// later anyway.
281						// node.debug("nodeRender ignored due to unrendered parent");
282						return;
283					}
284					// Create new <tr> after previous row
285					// if( node.isStatusNode() ) {
286					// 	newRow = tree.statusRowFragment.firstChild.cloneNode(true);
287					// } else {
288					newRow = tree.rowFragment.firstChild.cloneNode(true);
289					// }
290					prevNode = findPrevRowNode(node);
291					// $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key);
292					_assert(prevNode);
293					if (collapsed === true && _recursive) {
294						// hide all child rows, so we can use an animation to show it later
295						newRow.style.display = "none";
296					} else if (deep && ctx.hasCollapsedParents) {
297						// also hide this row if deep === true but any parent is collapsed
298						newRow.style.display = "none";
299						//					newRow.style.color = "red";
300					}
301					if (prevNode.tr) {
302						insertSiblingAfter(prevNode.tr, newRow);
303					} else {
304						_assert(
305							!prevNode.parent,
306							"prev. row must have a tr, or be system root"
307						);
308						// tree.tbody.appendChild(newRow);
309						insertFirstChild(tree.tbody, newRow); // #675
310					}
311					node.tr = newRow;
312					if (node.key && opts.generateIds) {
313						node.tr.id = opts.idPrefix + node.key;
314					}
315					node.tr.ftnode = node;
316					// if(opts.aria){
317					// 	$(node.tr).attr("aria-labelledby", "ftal_" + opts.idPrefix + node.key);
318					// }
319					node.span = $("span.fancytree-node", node.tr).get(0);
320					// Set icon, link, and title (normally this is only required on initial render)
321					this.nodeRenderTitle(ctx);
322					// Allow tweaking, binding, after node was created for the first time
323					//				tree._triggerNodeEvent("createNode", ctx);
324					if (opts.createNode) {
325						opts.createNode.call(tree, { type: "createNode" }, ctx);
326					}
327				}
328			}
329			// Allow tweaking after node state was rendered
330			//		tree._triggerNodeEvent("renderNode", ctx);
331			if (opts.renderNode) {
332				opts.renderNode.call(tree, { type: "renderNode" }, ctx);
333			}
334			// Visit child nodes
335			// Add child markup
336			children = node.children;
337			if (children && (isRootNode || deep || node.expanded)) {
338				for (i = 0, l = children.length; i < l; i++) {
339					subCtx = $.extend({}, ctx, { node: children[i] });
340					subCtx.hasCollapsedParents =
341						subCtx.hasCollapsedParents || !node.expanded;
342					this.nodeRender(subCtx, force, deep, collapsed, true);
343				}
344			}
345			// Make sure, that <tr> order matches node.children order.
346			if (children && !_recursive) {
347				// we only have to do it once, for the root branch
348				prevTr = node.tr || null;
349				firstTr = tree.tbody.firstChild;
350				// Iterate over all descendants
351				node.visit(function (n) {
352					if (n.tr) {
353						if (
354							!n.parent.expanded &&
355							n.tr.style.display !== "none"
356						) {
357							// fix after a node was dropped over a collapsed
358							n.tr.style.display = "none";
359							setChildRowVisibility(n, false);
360						}
361						if (n.tr.previousSibling !== prevTr) {
362							node.debug("_fixOrder: mismatch at node: " + n);
363							var nextTr = prevTr ? prevTr.nextSibling : firstTr;
364							tree.tbody.insertBefore(n.tr, nextTr);
365						}
366						prevTr = n.tr;
367					}
368				});
369			}
370			// Update element classes according to node state
371			// if(!isRootNode){
372			// 	this.nodeRenderStatus(ctx);
373			// }
374		},
375		nodeRenderTitle: function (ctx, title) {
376			var $cb,
377				res,
378				tree = ctx.tree,
379				node = ctx.node,
380				opts = ctx.options,
381				isStatusNode = node.isStatusNode();
382
383			res = this._super(ctx, title);
384
385			if (node.isRootNode()) {
386				return res;
387			}
388			// Move checkbox to custom column
389			if (
390				opts.checkbox &&
391				!isStatusNode &&
392				opts.table.checkboxColumnIdx != null
393			) {
394				$cb = $("span.fancytree-checkbox", node.span); //.detach();
395				$(node.tr)
396					.find("td")
397					.eq(+opts.table.checkboxColumnIdx)
398					.html($cb);
399			}
400			// Update element classes according to node state
401			this.nodeRenderStatus(ctx);
402
403			if (isStatusNode) {
404				if (opts.renderStatusColumns) {
405					// Let user code write column content
406					opts.renderStatusColumns.call(
407						tree,
408						{ type: "renderStatusColumns" },
409						ctx
410					);
411				} else if (opts.table.mergeStatusColumns && node.isTopLevel()) {
412					$(node.tr)
413						.find(">td")
414						.eq(0)
415						.prop("colspan", tree.columnCount)
416						.text(node.title)
417						.addClass("fancytree-status-merged")
418						.nextAll()
419						.remove();
420				} // else: default rendering for status node: leave other cells empty
421			} else if (opts.renderColumns) {
422				opts.renderColumns.call(tree, { type: "renderColumns" }, ctx);
423			}
424			return res;
425		},
426		nodeRenderStatus: function (ctx) {
427			var indent,
428				node = ctx.node,
429				opts = ctx.options;
430
431			this._super(ctx);
432
433			$(node.tr).removeClass("fancytree-node");
434			// indent
435			indent = (node.getLevel() - 1) * opts.table.indentation;
436			if (opts.rtl) {
437				$(node.span).css({ paddingRight: indent + "px" });
438			} else {
439				$(node.span).css({ paddingLeft: indent + "px" });
440			}
441		},
442		/* Expand node, return Deferred.promise. */
443		nodeSetExpanded: function (ctx, flag, callOpts) {
444			// flag defaults to true
445			flag = flag !== false;
446
447			if ((ctx.node.expanded && flag) || (!ctx.node.expanded && !flag)) {
448				// Expanded state isn't changed - just call base implementation
449				return this._superApply(arguments);
450			}
451
452			var dfd = new $.Deferred(),
453				subOpts = $.extend({}, callOpts, {
454					noEvents: true,
455					noAnimation: true,
456				});
457
458			callOpts = callOpts || {};
459
460			function _afterExpand(ok, args) {
461				// ctx.tree.info("ok:" + ok, args);
462				if (ok) {
463					// #1108 minExpandLevel: 2 together with table extension does not work
464					// don't call when 'ok' is false:
465					setChildRowVisibility(ctx.node, flag);
466					if (
467						flag &&
468						ctx.options.autoScroll &&
469						!callOpts.noAnimation &&
470						ctx.node.hasChildren()
471					) {
472						// Scroll down to last child, but keep current node visible
473						ctx.node
474							.getLastChild()
475							.scrollIntoView(true, { topNode: ctx.node })
476							.always(function () {
477								if (!callOpts.noEvents) {
478									ctx.tree._triggerNodeEvent(
479										flag ? "expand" : "collapse",
480										ctx
481									);
482								}
483								dfd.resolveWith(ctx.node);
484							});
485					} else {
486						if (!callOpts.noEvents) {
487							ctx.tree._triggerNodeEvent(
488								flag ? "expand" : "collapse",
489								ctx
490							);
491						}
492						dfd.resolveWith(ctx.node);
493					}
494				} else {
495					if (!callOpts.noEvents) {
496						ctx.tree._triggerNodeEvent(
497							flag ? "expand" : "collapse",
498							ctx
499						);
500					}
501					dfd.rejectWith(ctx.node);
502				}
503			}
504			// Call base-expand with disabled events and animation
505			this._super(ctx, flag, subOpts)
506				.done(function () {
507					_afterExpand(true, arguments);
508				})
509				.fail(function () {
510					_afterExpand(false, arguments);
511				});
512			return dfd.promise();
513		},
514		nodeSetStatus: function (ctx, status, message, details) {
515			if (status === "ok") {
516				var node = ctx.node,
517					firstChild = node.children ? node.children[0] : null;
518				if (firstChild && firstChild.isStatusNode()) {
519					$(firstChild.tr).remove();
520				}
521			}
522			return this._superApply(arguments);
523		},
524		treeClear: function (ctx) {
525			this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
526			return this._superApply(arguments);
527		},
528		treeDestroy: function (ctx) {
529			this.$container.find("tbody").empty();
530			if (this.$source) {
531				this.$source.removeClass("fancytree-helper-hidden");
532			}
533			return this._superApply(arguments);
534		},
535		/*,
536	treeSetFocus: function(ctx, flag) {
537//	        alert("treeSetFocus" + ctx.tree.$container);
538		ctx.tree.$container.focus();
539		$.ui.fancytree.focusTree = ctx.tree;
540	}*/
541	});
542	// Value returned by `require('jquery.fancytree..')`
543	return $.ui.fancytree;
544}); // End of closure
545