1/*!
2 * jquery.fancytree.grid.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 (http://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 FT = $.ui.fancytree,
35		_assert = FT.assert,
36		SCROLL_MODE = "wheel"; // 'wheel' | 'scroll'
37	// EPS = 1.0;
38
39	/*
40	 * [ext-grid] ...
41	 *
42	 * @alias Fancytree#_addScrollbar
43	 * @requires jquery.fancytree.grid.js
44	 */
45	function _addScrollbar(table) {
46		var sbWidth = 10,
47			$table = $(table),
48			position = $table.position(),
49			// top = $table.find("tbody").position().top,
50
51			$sb = $("<div>", {
52				class: "fancytree-scrollbar",
53				css: {
54					border: "1px solid gray",
55					position: "absolute",
56					top: position.top,
57					left: position.left + $table.width(),
58					width: sbWidth,
59					height: $table.find("tbody").height(),
60				},
61			});
62
63		$table
64			.css({
65				"margin-right": sbWidth,
66			})
67			.after($sb);
68
69		return $sb;
70	}
71
72	/*
73	 * [ext-grid] Invalidate renumber status, i.e. trigger renumber next time.
74	 *
75	 * @alias Fancytree#_renumberReset
76	 * @requires jquery.fancytree.grid.js
77	 */
78	$.ui.fancytree._FancytreeClass.prototype._renumberReset = function () {
79		// this.debug("_renumberReset()");
80		this.visibleNodeList = null;
81	};
82
83	/*
84	 * [ext-grid] Adjust the start value if the content would be outside otherwise.
85	 *
86	 * @alias Fancytree#_fixStart
87	 * @requires jquery.fancytree.grid.js
88	 */
89	$.ui.fancytree._FancytreeClass.prototype._fixStart = function (
90		start,
91		apply
92	) {
93		var vp = this.viewport,
94			nodeList = this.visibleNodeList;
95
96		start = start == null ? vp.start : start;
97		// this.debug("_fixStart(" + start + ", " + !!apply + ")");
98		var orgStart = start;
99		// Don't scroll down below bottom node
100		if (nodeList) {
101			start = Math.min(start, this.visibleNodeList.length - vp.count);
102			start = Math.max(start, 0, start);
103			if (start !== orgStart) {
104				this.debug("Adjust start " + orgStart + " => " + start);
105				if (apply) {
106					vp.start = start;
107				}
108			}
109		}
110		return start;
111	};
112
113	/*
114	 * [ext-grid] ...
115	 *
116	 * @alias Fancytree#_shiftViewport
117	 * @requires jquery.fancytree.grid.js
118	 */
119	$.ui.fancytree._FancytreeClass.prototype._shiftViewport = function (
120		mode,
121		ofs
122	) {
123		this.debug("_shiftViewport", mode, ofs);
124		switch (mode) {
125			case "vscroll":
126				if (ofs) {
127					this.setViewport({
128						start: this.viewport.start + (ofs > 0 ? 1 : -1),
129					});
130				}
131				break;
132
133			default:
134				throw Error("Invalid  mode: " + mode);
135		}
136	};
137
138	/**
139	 * [ext-grid] Return true if viewport cannot be scrolled down any further.
140	 *
141	 * @alias Fancytree#isViewportBottom
142	 * @requires jquery.fancytree.grid.js
143	 */
144	$.ui.fancytree._FancytreeClass.prototype.isViewportBottom = function () {
145		return (
146			this.viewport.start + this.viewport.count >=
147			this.visibleNodeList.length
148		);
149	};
150
151	/**
152	 * [ext-grid] Define a subset of rows/columns to display and redraw.
153	 *
154	 * @param {object | boolean} options viewport boundaries and status.
155	 *
156	 * @alias Fancytree#setViewport
157	 * @requires jquery.fancytree.grid.js
158	 */
159	$.ui.fancytree._FancytreeClass.prototype.setViewport = function (opts) {
160		if (typeof opts === "boolean") {
161			this.debug("setViewport( " + opts + ")");
162			return this.setViewport({ enabled: opts });
163		}
164		opts = opts || {};
165		var i,
166			count,
167			start,
168			newRow,
169			redrawReason = "",
170			vp = this.viewport,
171			diffVp = { start: 0, count: 0, enabled: null, force: null },
172			newVp = $.extend({}, vp),
173			trList = this.tbody.children,
174			trCount = trList.length;
175
176		// Sanitize viewport settings and check if we need to redraw
177		this.debug("setViewport(" + opts.start + ", +" + opts.count + ")");
178		if (opts.force) {
179			redrawReason += "force";
180			diffVp.force = true;
181		}
182
183		opts.enabled = opts.enabled !== false; // default to true
184		if (vp.enabled !== opts.enabled) {
185			redrawReason += "enable";
186			newVp.enabled = diffVp.enabled = opts.enabled;
187		}
188
189		start = opts.start == null ? vp.start : Math.max(0, +opts.start);
190		// Adjust start value to assure the current content is inside vp
191		start = this._fixStart(start, false);
192
193		if (vp.start !== +start) {
194			redrawReason += "start";
195			newVp.start = start;
196			diffVp.start = start - vp.start;
197		}
198
199		count = opts.count == null ? vp.count : Math.max(1, +opts.count);
200		if (vp.count !== +count) {
201			redrawReason += "count";
202			newVp.count = count;
203			diffVp.count = count - vp.count;
204		}
205		// if (vp.left !== +opts.left) {
206		// 	diffVp.left = left - vp.left;
207		// 	newVp.left = opts.left;
208		// 	redrawReason += "left";
209		// }
210		// if (vp.right !== +opts.right) {
211		// 	diffVp.right = right - vp.right;
212		// 	newVp.right = opts.right;
213		// 	redrawReason += "right";
214		// }
215
216		if (!redrawReason) {
217			return false;
218		}
219		// Let user cancel or modify the update
220		var info = {
221			next: newVp,
222			diff: diffVp,
223			reason: redrawReason,
224			scrollOnly: redrawReason === "start",
225		};
226		if (
227			!opts.noEvents &&
228			this._triggerTreeEvent("beforeUpdateViewport", null, info) === false
229		) {
230			return false;
231		}
232		info.prev = $.extend({}, vp);
233		delete info.next;
234		// vp.enabled = newVp.enabled;
235		vp.start = newVp.start;
236		vp.count = newVp.count;
237
238		// Make sure we have the correct count of TRs
239		var prevPhase = this.isVpUpdating;
240
241		if (trCount > count) {
242			for (i = 0; i < trCount - count; i++) {
243				delete this.tbody.lastChild.ftnode;
244				this.tbody.removeChild(this.tbody.lastChild);
245			}
246		} else if (trCount < count) {
247			for (i = 0; i < count - trCount; i++) {
248				newRow = this.rowFragment.firstChild.cloneNode(true);
249				this.tbody.appendChild(newRow);
250			}
251		}
252		trCount = trList.length;
253
254		// Update visible node cache if needed
255		var force = opts.force;
256		this.redrawViewport(force);
257
258		if (!opts.noEvents) {
259			this._triggerTreeEvent("updateViewport", null, info);
260		}
261
262		this.isVpUpdating = prevPhase;
263		return true;
264	};
265
266	/**
267	 * [ext-grid] Calculate the viewport count from current scroll wrapper height.
268	 *
269	 * @alias Fancytree#adjustViewportSize
270	 * @requires jquery.fancytree.grid.js
271	 */
272	$.ui.fancytree._FancytreeClass.prototype.adjustViewportSize = function () {
273		_assert(
274			this.scrollWrapper,
275			"No parent div.fancytree-grid-container found."
276		);
277		if (this.isVpUpdating) {
278			this.debug("Ignoring adjustViewportSize() during VP update.");
279			return;
280		}
281		// Calculate how many rows fit into current container height
282		var $table = this.$container,
283			wrapper = this.scrollWrapper,
284			trHeight = $table.find(">tbody>tr").first().height() || 0,
285			tableHeight = $table.height(),
286			headHeight = tableHeight - this.viewport.count * trHeight,
287			wrapperHeight = wrapper.offsetHeight,
288			free = wrapperHeight - headHeight,
289			newCount = trHeight ? Math.floor(free / trHeight) : 0;
290
291		// console.info(
292		// 	"set container height",
293		// 	$(this)
294		// 		.parent(".fancytree-grid-container")
295		// 		.height()
296		// );
297
298		this.setViewport({ count: newCount });
299		// if (SCROLL_MODE === "scroll") {
300		// 	// Add bottom margin to the table, to make sure the wrapper becomes
301		// 	// scrollable
302		// 	var mb = wrapperHeight - $table.height() - 2.0 * EPS;
303		// 	this.debug("margin-bottom=" + mb);
304		// 	$table.css("margin-bottom", mb);
305		// }
306	};
307
308	/*
309	 * [ext-grid] Calculate the scroll container dimension from the current tree table.
310	 *
311	 * @alias Fancytree#initViewportWrapper
312	 * @requires jquery.fancytree.grid.js
313	 */
314	$.ui.fancytree._FancytreeClass.prototype._initViewportWrapper =
315		function () {
316			var // wrapper = this.scrollWrapper,
317				// $wrapper = $(wrapper),
318				tree = this;
319
320			// if (SCROLL_MODE === "scroll") {
321			// 	$wrapper.on("scroll", function(e) {
322			// 		var viewport = tree.viewport,
323			// 			curTop = wrapper.scrollTop,
324			// 			homeTop = viewport.start === 0 ? 0 : EPS,
325			// 			dy = viewport.start === 0 ? 1 : curTop - EPS; //homeTop;
326
327			// 		tree.debug(
328			// 			"Got 'scroll' event: scrollTop=" +
329			// 				curTop +
330			// 				", homeTop=" +
331			// 				homeTop +
332			// 				", start=" +
333			// 				viewport.start +
334			// 				", dy=" +
335			// 				dy
336			// 		);
337			// 		if (tree.isVpUpdating) {
338			// 			tree.debug("Ignoring scroll during VP update.");
339			// 			return;
340			// 		} else if (curTop === homeTop) {
341			// 			tree.debug("Ignoring scroll to neutral " + homeTop + ".");
342			// 			return;
343			// 		}
344			// 		tree._shiftViewport("vscroll", dy);
345			// 		homeTop = viewport.start === 0 ? 0 : EPS;
346			// 		setTimeout(function() {
347			// 			tree.debug(
348			// 				"scrollTop(" +
349			// 					wrapper.scrollTop +
350			// 					" -> " +
351			// 					homeTop +
352			// 					")..."
353			// 			);
354			// 			wrapper.scrollTop = homeTop;
355			// 		}, 0);
356			// 	});
357			// }
358			if (SCROLL_MODE === "wheel") {
359				this.$container.on("wheel", function (e) {
360					var orgEvent = e.originalEvent,
361						viewport = tree.viewport,
362						dy = orgEvent.deltaY; // * orgEvent.wheelDeltaY;
363
364					if (
365						!dy ||
366						e.altKey ||
367						e.ctrlKey ||
368						e.metaKey ||
369						e.shiftKey
370					) {
371						return true;
372					}
373					if (dy < 0 && viewport.start === 0) {
374						return true;
375					}
376					if (dy > 0 && tree.isViewportBottom()) {
377						return true;
378					}
379					tree.debug(
380						"Got 'wheel' event: dy=" +
381							dy +
382							", mode=" +
383							orgEvent.deltaMode
384					);
385					tree._shiftViewport("vscroll", dy);
386					return false;
387				});
388			}
389		};
390
391	/*
392	 * [ext-grid] Renumber and collect all visible rows.
393	 *
394	 * @param {bool} [force=false]
395	 * @param {FancytreeNode | int} [startIdx=0]
396	 * @alias Fancytree#_renumberVisibleNodes
397	 * @requires jquery.fancytree.grid.js
398	 */
399	$.ui.fancytree._FancytreeClass.prototype._renumberVisibleNodes = function (
400		force,
401		startIdx
402	) {
403		if (
404			(!this.options.viewport.enabled || this.visibleNodeList != null) &&
405			force !== true
406		) {
407			// this.debug("_renumberVisibleNodes() ignored.");
408			return false;
409		}
410		this.debugTime("_renumberVisibleNodes()");
411		var i = 0,
412			prevLength = this.visibleNodeList ? this.visibleNodeList.length : 0,
413			visibleNodeList = (this.visibleNodeList = []);
414
415		// Reset previous data
416		this.visit(function (node) {
417			node._rowIdx = null;
418			// node.span = null;
419			// if (node.tr) {
420			// 	delete node.tr.ftnode;
421			// 	node.tr = null;
422			// }
423		});
424		// Iterate over all *visible* nodes
425		this.visitRows(function (node) {
426			node._rowIdx = i++;
427			visibleNodeList.push(node);
428		});
429		this.debugTimeEnd("_renumberVisibleNodes()");
430		if (i !== prevLength) {
431			this._triggerTreeEvent("updateViewport", null, {
432				reason: "renumber",
433				diff: { start: 0, count: 0, enabled: null, force: null },
434				next: $.extend({}, this.viewport),
435				// visibleCount: prevLength,
436				// cur: i,
437			});
438		}
439	};
440
441	/**
442	 * [ext-grid] Render all visible nodes into the viweport.
443	 *
444	 * @param {bool} [force=false]
445	 * @alias Fancytree#redrawViewport
446	 * @requires jquery.fancytree.grid.js
447	 */
448	$.ui.fancytree._FancytreeClass.prototype.redrawViewport = function (force) {
449		if (this._enableUpdate === false) {
450			// tree.debug("no render", tree._enableUpdate);
451			return;
452		}
453		this.debugTime("redrawViewport()");
454		this._renumberVisibleNodes(force);
455		// Adjust vp.start value to assure the current content is inside:
456		this._fixStart(null, true);
457
458		var i = 0,
459			vp = this.viewport,
460			visibleNodeList = this.visibleNodeList,
461			start = vp.start,
462			bottom = start + vp.count,
463			tr,
464			_renderCount = 0,
465			trIdx = 0,
466			trList = this.tbody.children,
467			prevPhase = this.isVpUpdating;
468
469		// Reset previous data
470		this.visit(function (node) {
471			// node.debug("redrawViewport(): _rowIdx=" + node._rowIdx);
472			node.span = null;
473			if (node.tr) {
474				delete node.tr.ftnode;
475				node.tr = null;
476			}
477		});
478
479		// Redraw the whole tree, erasing all node markup before and after
480		// the viewport
481
482		for (i = start; i < bottom; i++) {
483			var node = visibleNodeList[i];
484
485			tr = trList[trIdx];
486
487			if (!node) {
488				// TODO: make trailing empty rows configurable (custom template or remove TRs)
489				var newRow = this.rowFragment.firstChild.cloneNode(true);
490				this.tbody.replaceChild(newRow, tr);
491				trIdx++;
492				continue;
493			}
494			if (tr !== node.tr) {
495				node.tr = tr;
496				node.render();
497				_renderCount++;
498
499				// TODO:
500				// Implement scrolling by re-using existing markup
501				// e.g. shifting TRs or TR child elements instead of
502				// re-creating all the time
503			}
504			trIdx++;
505		}
506		this.isVpUpdating = prevPhase;
507		this.debugTimeEnd("redrawViewport()");
508	};
509
510	$.ui.fancytree.registerExtension({
511		name: "grid",
512		version: "2.38.3",
513		// Default options for this extension.
514		options: {
515			checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx)
516			indentation: 16, // indent every node level by 16px
517			mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR
518			nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0)
519		},
520		// Overide virtual methods for this extension.
521		// `this`       : is this extension object
522		// `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree)
523		treeInit: function (ctx) {
524			var i,
525				columnCount,
526				n,
527				$row,
528				$tbody,
529				tree = ctx.tree,
530				opts = ctx.options,
531				tableOpts = opts.table,
532				$table = tree.widget.element,
533				$scrollWrapper = $table.parent(".fancytree-grid-container");
534
535			if ($.inArray("table", opts.extensions) >= 0) {
536				$.error("ext-grid and ext-table are mutually exclusive.");
537			}
538			if (opts.renderStatusColumns === true) {
539				opts.renderStatusColumns = opts.renderColumns;
540			}
541			// Note: we also re-use CSS rules from ext-table
542			$table.addClass(
543				"fancytree-container fancytree-ext-grid fancytree-ext-table"
544			);
545			$tbody = $table.find(">tbody");
546			if (!$tbody.length) {
547				// TODO: not sure if we can rely on browsers to insert missing <tbody> before <tr>s:
548				if ($table.find(">tr").length) {
549					$.error(
550						"Expected table > tbody > tr. If you see this, please open an issue."
551					);
552				}
553				$tbody = $("<tbody>").appendTo($table);
554			}
555
556			tree.tbody = $tbody[0];
557
558			// Prepare row templates:
559			// Determine column count from table header if any
560			columnCount = $("thead >tr", $table).last().find(">th").length;
561			// Read TR templates from tbody if any
562			$row = $tbody.children("tr").first();
563			if ($row.length) {
564				n = $row.children("td").length;
565				if (columnCount && n !== columnCount) {
566					tree.warn(
567						"Column count mismatch between thead (" +
568							columnCount +
569							") and tbody (" +
570							n +
571							"): using tbody."
572					);
573					columnCount = n;
574				}
575				$row = $row.clone();
576			} else {
577				// Only thead is defined: create default row markup
578				_assert(
579					columnCount >= 1,
580					"Need either <thead> or <tbody> with <td> elements to determine column count."
581				);
582				$row = $("<tr />");
583				for (i = 0; i < columnCount; i++) {
584					$row.append("<td />");
585				}
586			}
587			$row.find(">td")
588				.eq(tableOpts.nodeColumnIdx)
589				.html("<span class='fancytree-node' />");
590			if (opts.aria) {
591				$row.attr("role", "row");
592				$row.find("td").attr("role", "gridcell");
593			}
594			tree.rowFragment = document.createDocumentFragment();
595			tree.rowFragment.appendChild($row.get(0));
596
597			$tbody.empty();
598
599			// Make sure that status classes are set on the node's <tr> elements
600			tree.statusClassPropName = "tr";
601			tree.ariaPropName = "tr";
602			this.nodeContainerAttrName = "tr";
603
604			// #489: make sure $container is set to <table>, even if ext-dnd is listed before ext-grid
605			tree.$container = $table;
606			if ($scrollWrapper.length) {
607				tree.scrollWrapper = $scrollWrapper[0];
608				this._initViewportWrapper();
609			} else {
610				tree.scrollWrapper = null;
611			}
612
613			// Scrolling is implemented completely differently here
614			$.ui.fancytree.overrideMethod(
615				$.ui.fancytree._FancytreeNodeClass.prototype,
616				"scrollIntoView",
617				function (effects, options) {
618					var node = this,
619						tree = node.tree,
620						topNode = options && options.topNode,
621						vp = tree.viewport,
622						start = vp ? vp.start : null;
623
624					if (!tree.viewport) {
625						return node._super.apply(this, arguments);
626					}
627					if (node._rowIdx < vp.start) {
628						start = node._rowIdx;
629					} else if (node._rowIdx >= vp.start + vp.count) {
630						start = node._rowIdx - vp.count + 1;
631					}
632					if (topNode && topNode._rowIdx < start) {
633						start = topNode._rowIdx;
634					}
635					tree.setViewport({ start: start });
636					// Return a resolved promise
637					return $.Deferred(function () {
638						this.resolveWith(node);
639					}).promise();
640				}
641			);
642
643			tree.visibleNodeList = null; // Set by _renumberVisibleNodes()
644			tree.viewport = {
645				enabled: true,
646				start: 0,
647				count: 10,
648				left: 0,
649				right: 0,
650			};
651			this.setViewport(
652				$.extend(
653					{
654						// enabled: true,
655						autoSize: true,
656						start: 0,
657						count: 10,
658						left: 0,
659						right: 0,
660						keepEmptyRows: true,
661						noEvents: true,
662					},
663					opts.viewport
664				)
665			);
666			// tree.$scrollbar = _addScrollbar($table);
667
668			this._superApply(arguments);
669
670			// standard Fancytree created a root UL
671			$(tree.rootNode.ul).remove();
672			tree.rootNode.ul = null;
673
674			// Add container to the TAB chain
675			// #577: Allow to set tabindex to "0", "-1" and ""
676			this.$container.attr("tabindex", opts.tabindex);
677			// this.$container.attr("tabindex", opts.tabbable ? "0" : "-1");
678			if (opts.aria) {
679				tree.$container
680					.attr("role", "treegrid")
681					.attr("aria-readonly", true);
682			}
683		},
684		nodeKeydown: function (ctx) {
685			var nextNode = null,
686				nextIdx = null,
687				tree = ctx.tree,
688				node = ctx.node,
689				nodeList = tree.visibleNodeList,
690				// treeOpts = ctx.options,
691				viewport = tree.viewport,
692				event = ctx.originalEvent,
693				eventString = FT.eventToString(event);
694
695			tree.debug("nodeKeydown(" + eventString + ")");
696
697			switch (eventString) {
698				case "home":
699				case "meta+up":
700					nextIdx = 0;
701					break;
702				case "end":
703				case "meta+down":
704					nextIdx = nodeList.length - 1;
705					break;
706				case "pageup":
707					nextIdx = node._rowIdx - viewport.count;
708					break;
709				case "pagedown":
710					nextIdx = node._rowIdx + viewport.count;
711					break;
712			}
713			if (nextIdx != null) {
714				nextIdx = Math.min(Math.max(0, nextIdx), nodeList.length - 1);
715				nextNode = nodeList[nextIdx];
716				nextNode.makeVisible();
717				nextNode.setActive();
718				return false;
719			}
720			return this._superApply(arguments);
721		},
722		nodeRemoveChildMarkup: function (ctx) {
723			var node = ctx.node;
724
725			node.visit(function (n) {
726				if (n.tr) {
727					delete n.tr.ftnode;
728					n.tr = null;
729					n.span = null;
730				}
731			});
732		},
733		nodeRemoveMarkup: function (ctx) {
734			var node = ctx.node;
735
736			if (node.tr) {
737				delete node.tr.ftnode;
738				node.tr = null;
739				node.span = null;
740			}
741			this.nodeRemoveChildMarkup(ctx);
742		},
743		/* Override standard render. */
744		nodeRender: function (ctx, force, deep, collapsed, _recursive) {
745			var children,
746				i,
747				l,
748				outsideViewport,
749				subCtx,
750				tree = ctx.tree,
751				node = ctx.node;
752
753			if (tree._enableUpdate === false) {
754				node.debug("nodeRender(): _enableUpdate: false");
755				return;
756			}
757			var opts = ctx.options,
758				viewport = tree.viewport.enabled ? tree.viewport : null,
759				start = viewport && viewport.start > 0 ? +viewport.start : 0,
760				bottom = viewport ? start + viewport.count - 1 : 0,
761				isRootNode = !node.parent;
762
763			_assert(viewport);
764
765			// node.debug("nodeRender(): " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
766			if (!_recursive) {
767				// node.debug("nodeRender(): start top node");
768				if (isRootNode && viewport) {
769					node.debug("nodeRender(): redrawViewport() instead");
770					return ctx.tree.redrawViewport();
771				}
772				ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
773				// Make sure visible row indices are up-to-date
774				if (viewport) {
775					tree._renumberVisibleNodes();
776				}
777			}
778
779			if (!isRootNode) {
780				outsideViewport =
781					viewport &&
782					(node._rowIdx < start ||
783						node._rowIdx >= start + viewport.count);
784
785				// node.debug(
786				// 	"nodeRender(): idx=" +
787				// 		node._rowIdx +
788				// 		", outside=" +
789				// 		outsideViewport +
790				// 		", TR count=" +
791				// 		tree.tbody.rows.length
792				// );
793				if (outsideViewport) {
794					// node.debug("nodeRender(): outsideViewport: ignored");
795					return;
796				}
797				if (!node.tr) {
798					if (node._rowIdx == null) {
799						// node.warn("nodeRender(): ignoring hidden");
800						return;
801					}
802					node.debug("nodeRender(): creating new TR.");
803					node.tr = tree.tbody.rows[node._rowIdx - start];
804				}
805				// _assert(
806				// 	node.tr,
807				// 	"nodeRender() called for node.tr == null: " + node
808				// );
809				node.tr.ftnode = node;
810
811				if (node.key && opts.generateIds) {
812					node.tr.id = opts.idPrefix + node.key;
813				}
814				node.span = $("span.fancytree-node", node.tr).get(0);
815
816				// Set icon, link, and title (normally this is only required on initial render)
817				// var ctx = this._makeHookContext(node);
818				this.nodeRenderTitle(ctx); // triggers renderColumns()
819
820				// Allow tweaking, binding, after node was created for the first time
821				if (opts.createNode) {
822					opts.createNode.call(this, { type: "createNode" }, ctx);
823				}
824			}
825			// Allow tweaking after node state was rendered
826			if (opts.renderNode) {
827				opts.renderNode.call(tree, { type: "renderNode" }, ctx);
828			}
829			// Visit child nodes
830			// Add child markup
831			children = node.children;
832			_assert(!deep, "deep is not supported");
833
834			if (children && (isRootNode || deep || node.expanded)) {
835				for (i = 0, l = children.length; i < l; i++) {
836					var child = children[i];
837
838					if (viewport && child._rowIdx > bottom) {
839						children[i].debug("BREAK render children loop");
840						return false;
841					}
842					subCtx = $.extend({}, ctx, { node: child });
843					subCtx.hasCollapsedParents =
844						subCtx.hasCollapsedParents || !node.expanded;
845					this.nodeRender(subCtx, force, deep, collapsed, true);
846				}
847			}
848		},
849		nodeRenderTitle: function (ctx, title) {
850			var $cb,
851				res,
852				tree = ctx.tree,
853				node = ctx.node,
854				opts = ctx.options,
855				isStatusNode = node.isStatusNode();
856
857			res = this._super(ctx, title);
858
859			if (node.isRootNode()) {
860				return res;
861			}
862			// Move checkbox to custom column
863			if (
864				opts.checkbox &&
865				!isStatusNode &&
866				opts.table.checkboxColumnIdx != null
867			) {
868				$cb = $("span.fancytree-checkbox", node.span); //.detach();
869				$(node.tr)
870					.find("td")
871					.eq(+opts.table.checkboxColumnIdx)
872					.html($cb);
873			}
874			// Update element classes according to node state
875			this.nodeRenderStatus(ctx);
876
877			if (isStatusNode) {
878				if (opts.renderStatusColumns) {
879					// Let user code write column content
880					opts.renderStatusColumns.call(
881						tree,
882						{ type: "renderStatusColumns" },
883						ctx
884					);
885				} else if (opts.grid.mergeStatusColumns && node.isTopLevel()) {
886					node.warn("mergeStatusColumns is not yet implemented.");
887					// This approach would not work, since the roe may be re-used:
888					// $(node.tr)
889					// 	.find(">td")
890					// 	.eq(0)
891					// 	.prop("colspan", tree.columnCount)
892					// 	.text(node.title)
893					// 	.addClass("fancytree-status-merged")
894					// 	.nextAll()
895					// 	.remove();
896				} // else: default rendering for status node: leave other cells empty
897			} else if (opts.renderColumns) {
898				opts.renderColumns.call(tree, { type: "renderColumns" }, ctx);
899			}
900			return res;
901		},
902		nodeRenderStatus: function (ctx) {
903			var indent,
904				node = ctx.node,
905				opts = ctx.options;
906
907			this._super(ctx);
908
909			$(node.tr).removeClass("fancytree-node");
910			// indent
911			indent = (node.getLevel() - 1) * opts.table.indentation;
912			if (opts.rtl) {
913				$(node.span).css({ paddingRight: indent + "px" });
914			} else {
915				$(node.span).css({ paddingLeft: indent + "px" });
916			}
917		},
918		/* Expand node, return Deferred.promise. */
919		nodeSetExpanded: function (ctx, flag, callOpts) {
920			var node = ctx.node,
921				tree = ctx.tree;
922
923			// flag defaults to true
924			flag = flag !== false;
925
926			if ((node.expanded && flag) || (!node.expanded && !flag)) {
927				// Expanded state isn't changed - just call base implementation
928				return this._superApply(arguments);
929			}
930
931			var dfd = new $.Deferred(),
932				subOpts = $.extend({}, callOpts, {
933					noEvents: true,
934					noAnimation: true,
935				});
936
937			callOpts = callOpts || {};
938
939			function _afterExpand(ok) {
940				tree.redrawViewport(true);
941
942				if (ok) {
943					if (
944						flag &&
945						ctx.options.autoScroll &&
946						!callOpts.noAnimation &&
947						node.hasChildren()
948					) {
949						// Scroll down to last child, but keep current node visible
950						node.getLastChild()
951							.scrollIntoView(true, { topNode: node })
952							.always(function () {
953								if (!callOpts.noEvents) {
954									tree._triggerNodeEvent(
955										flag ? "expand" : "collapse",
956										ctx
957									);
958								}
959								dfd.resolveWith(node);
960							});
961					} else {
962						if (!callOpts.noEvents) {
963							tree._triggerNodeEvent(
964								flag ? "expand" : "collapse",
965								ctx
966							);
967						}
968						dfd.resolveWith(node);
969					}
970				} else {
971					if (!callOpts.noEvents) {
972						tree._triggerNodeEvent(
973							flag ? "expand" : "collapse",
974							ctx
975						);
976					}
977					dfd.rejectWith(node);
978				}
979			}
980			// Call base-expand with disabled events and animation
981			this._super(ctx, flag, subOpts)
982				.done(function () {
983					_afterExpand(true);
984				})
985				.fail(function () {
986					_afterExpand(false);
987				});
988			return dfd.promise();
989		},
990		treeClear: function (ctx) {
991			// this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
992			// this._renumberReset(); // Invalidate visible row cache
993			return this._superApply(arguments);
994		},
995		treeDestroy: function (ctx) {
996			this.$container.find("tbody").empty();
997			this.$container.off("wheel");
998			if (this.$source) {
999				this.$source.removeClass("fancytree-helper-hidden");
1000			}
1001			this._renumberReset(); // Invalidate visible row cache
1002			return this._superApply(arguments);
1003		},
1004		treeStructureChanged: function (ctx, type) {
1005			// debugger;
1006			if (type !== "addNode" || ctx.tree.visibleNodeList) {
1007				// this.debug("treeStructureChanged(" + type + ")");
1008				this._renumberReset(); // Invalidate visible row cache
1009			}
1010		},
1011	});
1012	// Value returned by `require('jquery.fancytree..')`
1013	return $.ui.fancytree;
1014}); // End of closure
1015