1015 lines
26 KiB
JavaScript
1015 lines
26 KiB
JavaScript
/*!
|
|
* 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.2
|
|
* @date 2022-06-30T18:24:06Z
|
|
*/
|
|
|
|
(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 = $("<div>", {
|
|
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.2",
|
|
// 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 <tbody> before <tr>s:
|
|
if ($table.find(">tr").length) {
|
|
$.error(
|
|
"Expected table > tbody > tr. If you see this, please open an issue."
|
|
);
|
|
}
|
|
$tbody = $("<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 <thead> or <tbody> with <td> elements to determine column count."
|
|
);
|
|
$row = $("<tr />");
|
|
for (i = 0; i < columnCount; i++) {
|
|
$row.append("<td />");
|
|
}
|
|
}
|
|
$row.find(">td")
|
|
.eq(tableOpts.nodeColumnIdx)
|
|
.html("<span class='fancytree-node' />");
|
|
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 <tr> elements
|
|
tree.statusClassPropName = "tr";
|
|
tree.ariaPropName = "tr";
|
|
this.nodeContainerAttrName = "tr";
|
|
|
|
// #489: make sure $container is set to <table>, 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
|