/*! * jquery.fancytree.grid.js * * Render tree as table (aka 'tree grid', 'table tree'). * (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/) * * Copyright (c) 2008-2021, Martin Wendt (http://wwWendt.de) * * Released under the MIT license * https://github.com/mar10/fancytree/wiki/LicenseInfo * * @version 2.38.0 * @date 2021-02-09T20:03:49Z */ (function(factory) { if (typeof define === "function" && define.amd) { // AMD. Register as an anonymous module. define(["jquery", "./jquery.fancytree"], factory); } else if (typeof module === "object" && module.exports) { // Node/CommonJS require("./jquery.fancytree"); module.exports = factory(require("jquery")); } else { // Browser globals factory(jQuery); } })(function($) { "use strict"; /****************************************************************************** * Private functions and variables */ var FT = $.ui.fancytree, _assert = FT.assert, SCROLL_MODE = "wheel"; // 'wheel' | 'scroll' // EPS = 1.0; /* * [ext-grid] ... * * @alias Fancytree#_addScrollbar * @requires jquery.fancytree.grid.js */ function _addScrollbar(table) { var sbWidth = 10, $table = $(table), position = $table.position(), // top = $table.find("tbody").position().top, $sb = $("
", { class: "fancytree-scrollbar", css: { border: "1px solid gray", position: "absolute", top: position.top, left: position.left + $table.width(), width: sbWidth, height: $table.find("tbody").height(), }, }); $table .css({ "margin-right": sbWidth, }) .after($sb); return $sb; } /* * [ext-grid] Invalidate renumber status, i.e. trigger renumber next time. * * @alias Fancytree#_renumberReset * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype._renumberReset = function() { // this.debug("_renumberReset()"); this.visibleNodeList = null; }; /* * [ext-grid] Adjust the start value if the content would be outside otherwise. * * @alias Fancytree#_fixStart * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype._fixStart = function( start, apply ) { var vp = this.viewport, nodeList = this.visibleNodeList; start = start == null ? vp.start : start; // this.debug("_fixStart(" + start + ", " + !!apply + ")"); var orgStart = start; // Don't scroll down below bottom node if (nodeList) { start = Math.min(start, this.visibleNodeList.length - vp.count); start = Math.max(start, 0, start); if (start !== orgStart) { this.debug("Adjust start " + orgStart + " => " + start); if (apply) { vp.start = start; } } } return start; }; /* * [ext-grid] ... * * @alias Fancytree#_shiftViewport * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype._shiftViewport = function( mode, ofs ) { this.debug("_shiftViewport", mode, ofs); switch (mode) { case "vscroll": if (ofs) { this.setViewport({ start: this.viewport.start + (ofs > 0 ? 1 : -1), }); } break; default: throw Error("Invalid mode: " + mode); } }; /** * [ext-grid] Return true if viewport cannot be scrolled down any further. * * @alias Fancytree#isViewportBottom * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype.isViewportBottom = function() { return ( this.viewport.start + this.viewport.count >= this.visibleNodeList.length ); }; /** * [ext-grid] Define a subset of rows/columns to display and redraw. * * @param {object | boolean} options viewport boundaries and status. * * @alias Fancytree#setViewport * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype.setViewport = function(opts) { if (typeof opts === "boolean") { this.debug("setViewport( " + opts + ")"); return this.setViewport({ enabled: opts }); } opts = opts || {}; var i, count, start, newRow, redrawReason = "", vp = this.viewport, diffVp = { start: 0, count: 0, enabled: null, force: null }, newVp = $.extend({}, vp), trList = this.tbody.children, trCount = trList.length; // Sanitize viewport settings and check if we need to redraw this.debug("setViewport(" + opts.start + ", +" + opts.count + ")"); if (opts.force) { redrawReason += "force"; diffVp.force = true; } opts.enabled = opts.enabled !== false; // default to true if (vp.enabled !== opts.enabled) { redrawReason += "enable"; newVp.enabled = diffVp.enabled = opts.enabled; } start = opts.start == null ? vp.start : Math.max(0, +opts.start); // Adjust start value to assure the current content is inside vp start = this._fixStart(start, false); if (vp.start !== +start) { redrawReason += "start"; newVp.start = start; diffVp.start = start - vp.start; } count = opts.count == null ? vp.count : Math.max(1, +opts.count); if (vp.count !== +count) { redrawReason += "count"; newVp.count = count; diffVp.count = count - vp.count; } // if (vp.left !== +opts.left) { // diffVp.left = left - vp.left; // newVp.left = opts.left; // redrawReason += "left"; // } // if (vp.right !== +opts.right) { // diffVp.right = right - vp.right; // newVp.right = opts.right; // redrawReason += "right"; // } if (!redrawReason) { return false; } // Let user cancel or modify the update var info = { next: newVp, diff: diffVp, reason: redrawReason, scrollOnly: redrawReason === "start", }; if ( !opts.noEvents && this._triggerTreeEvent("beforeUpdateViewport", null, info) === false ) { return false; } info.prev = $.extend({}, vp); delete info.next; // vp.enabled = newVp.enabled; vp.start = newVp.start; vp.count = newVp.count; // Make sure we have the correct count of TRs var prevPhase = this.isVpUpdating; if (trCount > count) { for (i = 0; i < trCount - count; i++) { delete this.tbody.lastChild.ftnode; this.tbody.removeChild(this.tbody.lastChild); } } else if (trCount < count) { for (i = 0; i < count - trCount; i++) { newRow = this.rowFragment.firstChild.cloneNode(true); this.tbody.appendChild(newRow); } } trCount = trList.length; // Update visible node cache if needed var force = opts.force; this.redrawViewport(force); if (!opts.noEvents) { this._triggerTreeEvent("updateViewport", null, info); } this.isVpUpdating = prevPhase; return true; }; /** * [ext-grid] Calculate the viewport count from current scroll wrapper height. * * @alias Fancytree#adjustViewportSize * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype.adjustViewportSize = function() { _assert( this.scrollWrapper, "No parent div.fancytree-grid-container found." ); if (this.isVpUpdating) { this.debug("Ignoring adjustViewportSize() during VP update."); return; } // Calculate how many rows fit into current container height var $table = this.$container, wrapper = this.scrollWrapper, trHeight = $table .find(">tbody>tr") .first() .height() || 0, tableHeight = $table.height(), headHeight = tableHeight - this.viewport.count * trHeight, wrapperHeight = wrapper.offsetHeight, free = wrapperHeight - headHeight, newCount = trHeight ? Math.floor(free / trHeight) : 0; // console.info( // "set container height", // $(this) // .parent(".fancytree-grid-container") // .height() // ); this.setViewport({ count: newCount }); // if (SCROLL_MODE === "scroll") { // // Add bottom margin to the table, to make sure the wrapper becomes // // scrollable // var mb = wrapperHeight - $table.height() - 2.0 * EPS; // this.debug("margin-bottom=" + mb); // $table.css("margin-bottom", mb); // } }; /* * [ext-grid] Calculate the scroll container dimension from the current tree table. * * @alias Fancytree#initViewportWrapper * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype._initViewportWrapper = function() { var // wrapper = this.scrollWrapper, // $wrapper = $(wrapper), tree = this; // if (SCROLL_MODE === "scroll") { // $wrapper.on("scroll", function(e) { // var viewport = tree.viewport, // curTop = wrapper.scrollTop, // homeTop = viewport.start === 0 ? 0 : EPS, // dy = viewport.start === 0 ? 1 : curTop - EPS; //homeTop; // tree.debug( // "Got 'scroll' event: scrollTop=" + // curTop + // ", homeTop=" + // homeTop + // ", start=" + // viewport.start + // ", dy=" + // dy // ); // if (tree.isVpUpdating) { // tree.debug("Ignoring scroll during VP update."); // return; // } else if (curTop === homeTop) { // tree.debug("Ignoring scroll to neutral " + homeTop + "."); // return; // } // tree._shiftViewport("vscroll", dy); // homeTop = viewport.start === 0 ? 0 : EPS; // setTimeout(function() { // tree.debug( // "scrollTop(" + // wrapper.scrollTop + // " -> " + // homeTop + // ")..." // ); // wrapper.scrollTop = homeTop; // }, 0); // }); // } if (SCROLL_MODE === "wheel") { this.$container.on("wheel", function(e) { var orgEvent = e.originalEvent, viewport = tree.viewport, dy = orgEvent.deltaY; // * orgEvent.wheelDeltaY; if (!dy || e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return true; } if (dy < 0 && viewport.start === 0) { return true; } if (dy > 0 && tree.isViewportBottom()) { return true; } tree.debug( "Got 'wheel' event: dy=" + dy + ", mode=" + orgEvent.deltaMode ); tree._shiftViewport("vscroll", dy); return false; }); } }; /* * [ext-grid] Renumber and collect all visible rows. * * @param {bool} [force=false] * @param {FancytreeNode | int} [startIdx=0] * @alias Fancytree#_renumberVisibleNodes * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype._renumberVisibleNodes = function( force, startIdx ) { if ( (!this.options.viewport.enabled || this.visibleNodeList != null) && force !== true ) { // this.debug("_renumberVisibleNodes() ignored."); return false; } this.debugTime("_renumberVisibleNodes()"); var i = 0, prevLength = this.visibleNodeList ? this.visibleNodeList.length : 0, visibleNodeList = (this.visibleNodeList = []); // Reset previous data this.visit(function(node) { node._rowIdx = null; // node.span = null; // if (node.tr) { // delete node.tr.ftnode; // node.tr = null; // } }); // Iterate over all *visible* nodes this.visitRows(function(node) { node._rowIdx = i++; visibleNodeList.push(node); }); this.debugTimeEnd("_renumberVisibleNodes()"); if (i !== prevLength) { this._triggerTreeEvent("updateViewport", null, { reason: "renumber", diff: { start: 0, count: 0, enabled: null, force: null }, next: $.extend({}, this.viewport), // visibleCount: prevLength, // cur: i, }); } }; /** * [ext-grid] Render all visible nodes into the viweport. * * @param {bool} [force=false] * @alias Fancytree#redrawViewport * @requires jquery.fancytree.grid.js */ $.ui.fancytree._FancytreeClass.prototype.redrawViewport = function(force) { if (this._enableUpdate === false) { // tree.debug("no render", tree._enableUpdate); return; } this.debugTime("redrawViewport()"); this._renumberVisibleNodes(force); // Adjust vp.start value to assure the current content is inside: this._fixStart(null, true); var i = 0, vp = this.viewport, visibleNodeList = this.visibleNodeList, start = vp.start, bottom = start + vp.count, tr, _renderCount = 0, trIdx = 0, trList = this.tbody.children, prevPhase = this.isVpUpdating; // Reset previous data this.visit(function(node) { // node.debug("redrawViewport(): _rowIdx=" + node._rowIdx); node.span = null; if (node.tr) { delete node.tr.ftnode; node.tr = null; } }); // Redraw the whole tree, erasing all node markup before and after // the viewport for (i = start; i < bottom; i++) { var node = visibleNodeList[i]; tr = trList[trIdx]; if (!node) { // TODO: make trailing empty rows configurable (custom template or remove TRs) var newRow = this.rowFragment.firstChild.cloneNode(true); this.tbody.replaceChild(newRow, tr); trIdx++; continue; } if (tr !== node.tr) { node.tr = tr; node.render(); _renderCount++; // TODO: // Implement scrolling by re-using existing markup // e.g. shifting TRs or TR child elements instead of // re-creating all the time } trIdx++; } this.isVpUpdating = prevPhase; this.debugTimeEnd("redrawViewport()"); }; $.ui.fancytree.registerExtension({ name: "grid", version: "2.38.0", // Default options for this extension. options: { checkboxColumnIdx: null, // render the checkboxes into the this column index (default: nodeColumnIdx) indentation: 16, // indent every node level by 16px mergeStatusColumns: true, // display 'nodata', 'loading', 'error' centered in a single, merged TR nodeColumnIdx: 0, // render node expander, icon, and title to this column (default: #0) }, // Overide virtual methods for this extension. // `this` : is this extension object // `this._super`: the virtual function that was overriden (member of prev. extension or Fancytree) treeInit: function(ctx) { var i, columnCount, n, $row, $tbody, tree = ctx.tree, opts = ctx.options, tableOpts = opts.table, $table = tree.widget.element, $scrollWrapper = $table.parent(".fancytree-grid-container"); if ($.inArray("table", opts.extensions) >= 0) { $.error("ext-grid and ext-table are mutually exclusive."); } if (opts.renderStatusColumns === true) { opts.renderStatusColumns = opts.renderColumns; } // Note: we also re-use CSS rules from ext-table $table.addClass( "fancytree-container fancytree-ext-grid fancytree-ext-table" ); $tbody = $table.find(">tbody"); if (!$tbody.length) { // TODO: not sure if we can rely on browsers to insert missing before s: if ($table.find(">tr").length) { $.error( "Expected table > tbody > tr. If you see this, please open an issue." ); } $tbody = $("").appendTo($table); } tree.tbody = $tbody[0]; // Prepare row templates: // Determine column count from table header if any columnCount = $("thead >tr", $table) .last() .find(">th").length; // Read TR templates from tbody if any $row = $tbody.children("tr").first(); if ($row.length) { n = $row.children("td").length; if (columnCount && n !== columnCount) { tree.warn( "Column count mismatch between thead (" + columnCount + ") and tbody (" + n + "): using tbody." ); columnCount = n; } $row = $row.clone(); } else { // Only thead is defined: create default row markup _assert( columnCount >= 1, "Need either or with elements to determine column count." ); $row = $(""); for (i = 0; i < columnCount; i++) { $row.append(""); } } $row.find(">td") .eq(tableOpts.nodeColumnIdx) .html(""); if (opts.aria) { $row.attr("role", "row"); $row.find("td").attr("role", "gridcell"); } tree.rowFragment = document.createDocumentFragment(); tree.rowFragment.appendChild($row.get(0)); $tbody.empty(); // Make sure that status classes are set on the node's elements tree.statusClassPropName = "tr"; tree.ariaPropName = "tr"; this.nodeContainerAttrName = "tr"; // #489: make sure $container is set to , even if ext-dnd is listed before ext-grid tree.$container = $table; if ($scrollWrapper.length) { tree.scrollWrapper = $scrollWrapper[0]; this._initViewportWrapper(); } else { tree.scrollWrapper = null; } // Scrolling is implemented completely differently here $.ui.fancytree.overrideMethod( $.ui.fancytree._FancytreeNodeClass.prototype, "scrollIntoView", function(effects, options) { var node = this, tree = node.tree, topNode = options && options.topNode, vp = tree.viewport, start = vp ? vp.start : null; if (!tree.viewport) { return node._super.apply(this, arguments); } if (node._rowIdx < vp.start) { start = node._rowIdx; } else if (node._rowIdx >= vp.start + vp.count) { start = node._rowIdx - vp.count + 1; } if (topNode && topNode._rowIdx < start) { start = topNode._rowIdx; } tree.setViewport({ start: start }); // Return a resolved promise return $.Deferred(function() { this.resolveWith(node); }).promise(); } ); tree.visibleNodeList = null; // Set by _renumberVisibleNodes() tree.viewport = { enabled: true, start: 0, count: 10, left: 0, right: 0, }; this.setViewport( $.extend( { // enabled: true, autoSize: true, start: 0, count: 10, left: 0, right: 0, keepEmptyRows: true, noEvents: true, }, opts.viewport ) ); // tree.$scrollbar = _addScrollbar($table); this._superApply(arguments); // standard Fancytree created a root UL $(tree.rootNode.ul).remove(); tree.rootNode.ul = null; // Add container to the TAB chain // #577: Allow to set tabindex to "0", "-1" and "" this.$container.attr("tabindex", opts.tabindex); // this.$container.attr("tabindex", opts.tabbable ? "0" : "-1"); if (opts.aria) { tree.$container .attr("role", "treegrid") .attr("aria-readonly", true); } }, nodeKeydown: function(ctx) { var nextNode = null, nextIdx = null, tree = ctx.tree, node = ctx.node, nodeList = tree.visibleNodeList, // treeOpts = ctx.options, viewport = tree.viewport, event = ctx.originalEvent, eventString = FT.eventToString(event); tree.debug("nodeKeydown(" + eventString + ")"); switch (eventString) { case "home": case "meta+up": nextIdx = 0; break; case "end": case "meta+down": nextIdx = nodeList.length - 1; break; case "pageup": nextIdx = node._rowIdx - viewport.count; break; case "pagedown": nextIdx = node._rowIdx + viewport.count; break; } if (nextIdx != null) { nextIdx = Math.min(Math.max(0, nextIdx), nodeList.length - 1); nextNode = nodeList[nextIdx]; nextNode.makeVisible(); nextNode.setActive(); return false; } return this._superApply(arguments); }, nodeRemoveChildMarkup: function(ctx) { var node = ctx.node; node.visit(function(n) { if (n.tr) { delete n.tr.ftnode; n.tr = null; n.span = null; } }); }, nodeRemoveMarkup: function(ctx) { var node = ctx.node; if (node.tr) { delete node.tr.ftnode; node.tr = null; node.span = null; } this.nodeRemoveChildMarkup(ctx); }, /* Override standard render. */ nodeRender: function(ctx, force, deep, collapsed, _recursive) { var children, i, l, outsideViewport, subCtx, tree = ctx.tree, node = ctx.node; if (tree._enableUpdate === false) { node.debug("nodeRender(): _enableUpdate: false"); return; } var opts = ctx.options, viewport = tree.viewport.enabled ? tree.viewport : null, start = viewport && viewport.start > 0 ? +viewport.start : 0, bottom = viewport ? start + viewport.count - 1 : 0, isRootNode = !node.parent; _assert(viewport); // node.debug("nodeRender(): " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr)); if (!_recursive) { // node.debug("nodeRender(): start top node"); if (isRootNode && viewport) { node.debug("nodeRender(): redrawViewport() instead"); return ctx.tree.redrawViewport(); } ctx.hasCollapsedParents = node.parent && !node.parent.expanded; // Make sure visible row indices are up-to-date if (viewport) { tree._renumberVisibleNodes(); } } if (!isRootNode) { outsideViewport = viewport && (node._rowIdx < start || node._rowIdx >= start + viewport.count); // node.debug( // "nodeRender(): idx=" + // node._rowIdx + // ", outside=" + // outsideViewport + // ", TR count=" + // tree.tbody.rows.length // ); if (outsideViewport) { // node.debug("nodeRender(): outsideViewport: ignored"); return; } if (!node.tr) { if (node._rowIdx == null) { // node.warn("nodeRender(): ignoring hidden"); return; } node.debug("nodeRender(): creating new TR."); node.tr = tree.tbody.rows[node._rowIdx - start]; } // _assert( // node.tr, // "nodeRender() called for node.tr == null: " + node // ); node.tr.ftnode = node; if (node.key && opts.generateIds) { node.tr.id = opts.idPrefix + node.key; } node.span = $("span.fancytree-node", node.tr).get(0); // Set icon, link, and title (normally this is only required on initial render) // var ctx = this._makeHookContext(node); this.nodeRenderTitle(ctx); // triggers renderColumns() // Allow tweaking, binding, after node was created for the first time if (opts.createNode) { opts.createNode.call(this, { type: "createNode" }, ctx); } } // Allow tweaking after node state was rendered if (opts.renderNode) { opts.renderNode.call(tree, { type: "renderNode" }, ctx); } // Visit child nodes // Add child markup children = node.children; _assert(!deep, "deep is not supported"); if (children && (isRootNode || deep || node.expanded)) { for (i = 0, l = children.length; i < l; i++) { var child = children[i]; if (viewport && child._rowIdx > bottom) { children[i].debug("BREAK render children loop"); return false; } subCtx = $.extend({}, ctx, { node: child }); subCtx.hasCollapsedParents = subCtx.hasCollapsedParents || !node.expanded; this.nodeRender(subCtx, force, deep, collapsed, true); } } }, nodeRenderTitle: function(ctx, title) { var $cb, res, tree = ctx.tree, node = ctx.node, opts = ctx.options, isStatusNode = node.isStatusNode(); res = this._super(ctx, title); if (node.isRootNode()) { return res; } // Move checkbox to custom column if ( opts.checkbox && !isStatusNode && opts.table.checkboxColumnIdx != null ) { $cb = $("span.fancytree-checkbox", node.span); //.detach(); $(node.tr) .find("td") .eq(+opts.table.checkboxColumnIdx) .html($cb); } // Update element classes according to node state this.nodeRenderStatus(ctx); if (isStatusNode) { if (opts.renderStatusColumns) { // Let user code write column content opts.renderStatusColumns.call( tree, { type: "renderStatusColumns" }, ctx ); } else if (opts.grid.mergeStatusColumns && node.isTopLevel()) { node.warn("mergeStatusColumns is not yet implemented."); // This approach would not work, since the roe may be re-used: // $(node.tr) // .find(">td") // .eq(0) // .prop("colspan", tree.columnCount) // .text(node.title) // .addClass("fancytree-status-merged") // .nextAll() // .remove(); } // else: default rendering for status node: leave other cells empty } else if (opts.renderColumns) { opts.renderColumns.call(tree, { type: "renderColumns" }, ctx); } return res; }, nodeRenderStatus: function(ctx) { var indent, node = ctx.node, opts = ctx.options; this._super(ctx); $(node.tr).removeClass("fancytree-node"); // indent indent = (node.getLevel() - 1) * opts.table.indentation; if (opts.rtl) { $(node.span).css({ paddingRight: indent + "px" }); } else { $(node.span).css({ paddingLeft: indent + "px" }); } }, /* Expand node, return Deferred.promise. */ nodeSetExpanded: function(ctx, flag, callOpts) { var node = ctx.node, tree = ctx.tree; // flag defaults to true flag = flag !== false; if ((node.expanded && flag) || (!node.expanded && !flag)) { // Expanded state isn't changed - just call base implementation return this._superApply(arguments); } var dfd = new $.Deferred(), subOpts = $.extend({}, callOpts, { noEvents: true, noAnimation: true, }); callOpts = callOpts || {}; function _afterExpand(ok) { tree.redrawViewport(true); if (ok) { if ( flag && ctx.options.autoScroll && !callOpts.noAnimation && node.hasChildren() ) { // Scroll down to last child, but keep current node visible node.getLastChild() .scrollIntoView(true, { topNode: node }) .always(function() { if (!callOpts.noEvents) { tree._triggerNodeEvent( flag ? "expand" : "collapse", ctx ); } dfd.resolveWith(node); }); } else { if (!callOpts.noEvents) { tree._triggerNodeEvent( flag ? "expand" : "collapse", ctx ); } dfd.resolveWith(node); } } else { if (!callOpts.noEvents) { tree._triggerNodeEvent( flag ? "expand" : "collapse", ctx ); } dfd.rejectWith(node); } } // Call base-expand with disabled events and animation this._super(ctx, flag, subOpts) .done(function() { _afterExpand(true); }) .fail(function() { _afterExpand(false); }); return dfd.promise(); }, treeClear: function(ctx) { // this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode)); // this._renumberReset(); // Invalidate visible row cache return this._superApply(arguments); }, treeDestroy: function(ctx) { this.$container.find("tbody").empty(); this.$container.off("wheel"); if (this.$source) { this.$source.removeClass("fancytree-helper-hidden"); } this._renumberReset(); // Invalidate visible row cache return this._superApply(arguments); }, treeStructureChanged: function(ctx, type) { // debugger; if (type !== "addNode" || ctx.tree.visibleNodeList) { // this.debug("treeStructureChanged(" + type + ")"); this._renumberReset(); // Invalidate visible row cache } }, }); // Value returned by `require('jquery.fancytree..')` return $.ui.fancytree; }); // End of closure