src/jquery.fancytree.fixed.js
/*!
* jquery.fancytree.fixed.js
*
* Add fixed colums and headers to ext.table.
* (Extension module for jquery.fancytree.js: https://github.com/mar10/fancytree/)
*
* Copyright (c) 2008-2023, Martin Wendt (https://wwWendt.de)
*
* Released under the MIT license
* https://github.com/mar10/fancytree/wiki/LicenseInfo
*
* @version @VERSION
* @date @DATE
*/
// Allow to use multiple var statements inside a function
(function (factory) {
if (typeof define === "function" && define.amd) {
// AMD. Register as an anonymous module.
define([
"jquery",
"./jquery.fancytree",
"./jquery.fancytree.table",
], factory);
} else if (typeof module === "object" && module.exports) {
// Node/CommonJS
require("./jquery.fancytree.table"); // core + table
module.exports = factory(require("jquery"));
} else {
// Browser globals
factory(jQuery);
}
})(function ($) {
"use strict";
/******************************************************************************
* Private functions and variables
*/
$.ui.fancytree.registerExtension({
name: "fixed",
version: "0.0.1",
// Default options for this extension.
options: {
fixCol: 1,
fixColWidths: null,
fixRows: true,
scrollSpeed: 50,
resizable: true,
classNames: {
table: "fancytree-ext-fixed",
wrapper: "fancytree-ext-fixed-wrapper",
topLeft: "fancytree-ext-fixed-wrapper-tl",
topRight: "fancytree-ext-fixed-wrapper-tr",
bottomLeft: "fancytree-ext-fixed-wrapper-bl",
bottomRight: "fancytree-ext-fixed-wrapper-br",
hidden: "fancytree-ext-fixed-hidden",
counterpart: "fancytree-ext-fixed-node-counterpart",
scrollBorderBottom: "fancytree-ext-fixed-scroll-border-bottom",
scrollBorderRight: "fancytree-ext-fixed-scroll-border-right",
hover: "fancytree-ext-fixed-hover",
},
},
// 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) {
this._requireExtension("table", true, true);
// 'fixed' requires the table extension to be loaded before itself
var res = this._superApply(arguments),
tree = ctx.tree,
options = this.options.fixed,
fcn = this.options.fixed.classNames,
$table = tree.widget.element,
fixedColCount = options.fixCols,
fixedRowCount = options.fixRows,
$tableWrapper = $table.parent(),
$topLeftWrapper = $("<div>").addClass(fcn.topLeft),
$topRightWrapper = $("<div>").addClass(fcn.topRight),
$bottomLeftWrapper = $("<div>").addClass(fcn.bottomLeft),
$bottomRightWrapper = $("<div>").addClass(fcn.bottomRight),
tableStyle = $table.attr("style"),
tableClass = $table.attr("class"),
$topLeftTable = $("<table>")
.attr("style", tableStyle)
.attr("class", tableClass),
$topRightTable = $("<table>")
.attr("style", tableStyle)
.attr("class", tableClass),
$bottomLeftTable = $table,
$bottomRightTable = $("<table>")
.attr("style", tableStyle)
.attr("class", tableClass),
$head = $table.find("thead"),
$colgroup = $table.find("colgroup"),
headRowCount = $head.find("tr").length;
this.$fixedWrapper = $tableWrapper;
$table.addClass(fcn.table);
$tableWrapper.addClass(fcn.wrapper);
$bottomRightTable.append($("<tbody>"));
if ($colgroup.length) {
$colgroup.remove();
}
if (typeof fixedRowCount === "boolean") {
fixedRowCount = fixedRowCount ? headRowCount : 0;
} else {
fixedRowCount = Math.max(
0,
Math.min(fixedRowCount, headRowCount)
);
}
if (fixedRowCount) {
$topLeftTable.append($head.clone(true));
$topRightTable.append($head.clone(true));
$head.remove();
}
$topLeftTable.find("tr").each(function (idx) {
$(this).find("th").slice(fixedColCount).remove();
});
$topRightTable.find("tr").each(function (idx) {
$(this).find("th").slice(0, fixedColCount).remove();
});
this.$fixedWrapper = $tableWrapper;
$tableWrapper.append(
$topLeftWrapper.append($topLeftTable),
$topRightWrapper.append($topRightTable),
$bottomLeftWrapper.append($bottomLeftTable),
$bottomRightWrapper.append($bottomRightTable)
);
$bottomRightTable.on("keydown", function (evt) {
var node = tree.focusNode,
ctx = tree._makeHookContext(node || tree, evt),
res = tree._callHook("nodeKeydown", ctx);
return res;
});
$bottomRightTable.on("click dblclick", "tr", function (evt) {
var $trLeft = $(this),
$trRight = $trLeft.data(fcn.counterpart),
node = $.ui.fancytree.getNode($trRight),
ctx = tree._makeHookContext(node, evt),
et = $.ui.fancytree.getEventTarget(evt),
prevPhase = tree.phase;
try {
tree.phase = "userEvent";
switch (evt.type) {
case "click":
ctx.targetType = et.type;
if (node.isPagingNode()) {
return (
tree._triggerNodeEvent(
"clickPaging",
ctx,
evt
) === true
);
}
return tree._triggerNodeEvent("click", ctx, evt) ===
false
? false
: tree._callHook("nodeClick", ctx);
case "dblclick":
ctx.targetType = et.type;
return tree._triggerNodeEvent(
"dblclick",
ctx,
evt
) === false
? false
: tree._callHook("nodeDblclick", ctx);
}
} finally {
tree.phase = prevPhase;
}
});
$tableWrapper
.on(
"mouseenter",
"." +
fcn.bottomRight +
" table tr, ." +
fcn.bottomLeft +
" table tr",
function (evt) {
var $tr = $(this),
$trOther = $tr.data(fcn.counterpart);
$tr.addClass(fcn.hover);
$trOther.addClass(fcn.hover);
}
)
.on(
"mouseleave",
"." +
fcn.bottomRight +
" table tr, ." +
fcn.bottomLeft +
" table tr",
function (evt) {
var $tr = $(this),
$trOther = $tr.data(fcn.counterpart);
$tr.removeClass(fcn.hover);
$trOther.removeClass(fcn.hover);
}
);
$bottomLeftWrapper.on(
"mousewheel DOMMouseScroll",
function (event) {
var $this = $(this),
newScroll = $this.scrollTop(),
scrollUp =
event.originalEvent.wheelDelta > 0 ||
event.originalEvent.detail < 0;
newScroll += scrollUp
? -options.scrollSpeed
: options.scrollSpeed;
$this.scrollTop(newScroll);
$bottomRightWrapper.scrollTop(newScroll);
event.preventDefault();
}
);
$bottomRightWrapper.scroll(function () {
var $this = $(this),
scrollLeft = $this.scrollLeft(),
scrollTop = $this.scrollTop();
$topLeftWrapper
.toggleClass(fcn.scrollBorderBottom, scrollTop > 0)
.toggleClass(fcn.scrollBorderRight, scrollLeft > 0);
$topRightWrapper
.toggleClass(fcn.scrollBorderBottom, scrollTop > 0)
.scrollLeft(scrollLeft);
$bottomLeftWrapper
.toggleClass(fcn.scrollBorderRight, scrollLeft > 0)
.scrollTop(scrollTop);
});
$.ui.fancytree.overrideMethod(
$.ui.fancytree._FancytreeNodeClass.prototype,
"scrollIntoView",
function (effects, options) {
var $prevContainer = tree.$container;
tree.$container = $bottomRightWrapper;
return this._super
.apply(this, arguments)
.always(function () {
tree.$container = $prevContainer;
});
}
);
return res;
},
treeLoad: function (ctx) {
var self = this,
res = this._superApply(arguments);
res.done(function () {
self.ext.fixed._adjustLayout.call(self);
if (self.options.fixed.resizable) {
self.ext.fixed._makeTableResizable();
}
});
return res;
},
_makeTableResizable: function () {
var $wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames,
$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
$topRightWrapper = $wrapper.find("div." + fcn.topRight),
$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft),
$bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight);
function _makeResizable($table) {
$table.resizable({
handles: "e",
resize: function (evt, ui) {
var width = Math.max($table.width(), ui.size.width);
$bottomLeftWrapper.css("width", width);
$topLeftWrapper.css("width", width);
$bottomRightWrapper.css("left", width);
$topRightWrapper.css("left", width);
},
stop: function () {
$table.css("width", "100%");
},
});
}
_makeResizable($topLeftWrapper.find("table"));
_makeResizable($bottomLeftWrapper.find("table"));
},
/* Called by nodeRender to sync node order with tag order.*/
// nodeFixOrder: function(ctx) {
// },
nodeLoadChildren: function (ctx, source) {
return this._superApply(arguments);
},
nodeRemoveChildMarkup: function (ctx) {
var node = ctx.node;
function _removeChild(elem) {
var i,
child,
children = elem.children;
if (children) {
for (i = 0; i < children.length; i++) {
child = children[i];
if (child.trRight) {
$(child.trRight).remove();
}
_removeChild(child);
}
}
}
_removeChild(node);
return this._superApply(arguments);
},
nodeRemoveMarkup: function (ctx) {
var node = ctx.node;
if (node.trRight) {
$(node.trRight).remove();
}
return this._superApply(arguments);
},
nodeSetActive: function (ctx, flag, callOpts) {
var node = ctx.node,
cn = this.options._classNames;
if (node.trRight) {
$(node.trRight)
.toggleClass(cn.active, flag)
.toggleClass(cn.focused, flag);
}
return this._superApply(arguments);
},
nodeKeydown: function (ctx) {
return this._superApply(arguments);
},
nodeSetFocus: function (ctx, flag) {
var node = ctx.node,
cn = this.options._classNames;
if (node.trRight) {
$(node.trRight).toggleClass(cn.focused, flag);
}
return this._superApply(arguments);
},
nodeRender: function (ctx, force, deep, collapsed, _recursive) {
var res = this._superApply(arguments),
node = ctx.node,
isRootNode = !node.parent;
if (!isRootNode && this.$fixedWrapper) {
var $trLeft = $(node.tr),
fcn = this.options.fixed.classNames,
$trRight = $trLeft.data(fcn.counterpart);
if (!$trRight && $trLeft.length) {
var idx = $trLeft.index(),
fixedColCount = this.options.fixed.fixCols,
$blTableBody = this.$fixedWrapper.find(
"div." + fcn.bottomLeft + " table tbody"
),
$brTableBody = this.$fixedWrapper.find(
"div." + fcn.bottomRight + " table tbody"
),
$prevLeftNode = $blTableBody
.find("tr")
.eq(Math.max(idx + 1, 0)),
prevRightNode = $prevLeftNode.data(fcn.counterpart);
$trRight = $trLeft.clone(true);
var trRight = $trRight.get(0);
if (prevRightNode) {
$(prevRightNode).before($trRight);
} else {
$brTableBody.append($trRight);
}
$trRight.show();
trRight.ftnode = node;
node.trRight = trRight;
$trLeft.find("td").slice(fixedColCount).remove();
$trRight.find("td").slice(0, fixedColCount).remove();
$trLeft.data(fcn.counterpart, $trRight);
$trRight.data(fcn.counterpart, $trLeft);
}
}
return res;
},
nodeRenderTitle: function (ctx, title) {
return this._superApply(arguments);
},
nodeRenderStatus: function (ctx) {
var res = this._superApply(arguments),
node = ctx.node;
if (node.trRight) {
var $trRight = $(node.trRight),
$trLeft = $(node.tr),
fcn = this.options.fixed.classNames,
hovering = $trRight.hasClass(fcn.hover),
trClasses = $trLeft.attr("class");
$trRight.attr("class", trClasses);
if (hovering) {
$trRight.addClass(fcn.hover);
$trLeft.addClass(fcn.hover);
}
}
return res;
},
nodeSetExpanded: function (ctx, flag, callOpts) {
var res,
self = this,
node = ctx.node,
$leftTr = $(node.tr),
fcn = this.options.fixed.classNames,
cn = this.options._classNames,
$rightTr = $leftTr.data(fcn.counterpart);
flag = typeof flag === "undefined" ? true : flag;
if (!$rightTr) {
return this._superApply(arguments);
}
$rightTr.toggleClass(cn.expanded, !!flag);
if (flag && !node.isExpanded()) {
res = this._superApply(arguments);
res.done(function () {
node.visit(function (child) {
var $trLeft = $(child.tr),
$trRight = $trLeft.data(fcn.counterpart);
self.ext.fixed._adjustRowHeight($trLeft, $trRight);
if (!child.expanded) {
return "skip";
}
});
self.ext.fixed._adjustColWidths();
self.ext.fixed._adjustWrapperLayout();
});
} else if (!flag && node.isExpanded()) {
node.visit(function (child) {
var $trLeft = $(child.tr),
$trRight = $trLeft.data(fcn.counterpart);
if ($trRight) {
if (!child.expanded) {
return "skip";
}
}
});
self.ext.fixed._adjustColWidths();
self.ext.fixed._adjustWrapperLayout();
res = this._superApply(arguments);
} else {
res = this._superApply(arguments);
}
return res;
},
nodeSetStatus: function (ctx, status, message, details) {
return this._superApply(arguments);
},
treeClear: function (ctx) {
var tree = ctx.tree,
$table = tree.widget.element,
$wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames;
$table.find("tr, td, th, thead").removeClass(fcn.hidden).css({
"min-width": "auto",
height: "auto",
});
$wrapper.empty().append($table);
return this._superApply(arguments);
},
treeRegisterNode: function (ctx, add, node) {
return this._superApply(arguments);
},
treeDestroy: function (ctx) {
var tree = ctx.tree,
$table = tree.widget.element,
$wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames;
$table.find("tr, td, th, thead").removeClass(fcn.hidden).css({
"min-width": "auto",
height: "auto",
});
$wrapper.empty().append($table);
return this._superApply(arguments);
},
_adjustColWidths: function () {
if (this.options.fixed.adjustColWidths) {
this.options.fixed.adjustColWidths.call(this);
return;
}
var $wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames,
$tlWrapper = $wrapper.find("div." + fcn.topLeft),
$blWrapper = $wrapper.find("div." + fcn.bottomLeft),
$trWrapper = $wrapper.find("div." + fcn.topRight),
$brWrapper = $wrapper.find("div." + fcn.bottomRight);
function _adjust($topWrapper, $bottomWrapper) {
var $trTop = $topWrapper.find("thead tr").first(),
$trBottom = $bottomWrapper.find("tbody tr").first();
$trTop.find("th").each(function (idx) {
var $thTop = $(this),
$tdBottom = $trBottom.find("td").eq(idx),
thTopWidth = $thTop.width(),
thTopOuterWidth = $thTop.outerWidth(),
tdBottomWidth = $tdBottom.width(),
tdBottomOuterWidth = $tdBottom.outerWidth(),
newWidth = Math.max(
thTopOuterWidth,
tdBottomOuterWidth
);
$thTop.css(
"min-width",
newWidth - (thTopOuterWidth - thTopWidth)
);
$tdBottom.css(
"min-width",
newWidth - (tdBottomOuterWidth - tdBottomWidth)
);
});
}
_adjust($tlWrapper, $blWrapper);
_adjust($trWrapper, $brWrapper);
},
_adjustRowHeight: function ($tr1, $tr2) {
var fcn = this.options.fixed.classNames;
if (!$tr2) {
$tr2 = $tr1.data(fcn.counterpart);
}
$tr1.css("height", "auto");
$tr2.css("height", "auto");
var row1Height = $tr1.outerHeight(),
row2Height = $tr2.outerHeight(),
newHeight = Math.max(row1Height, row2Height);
$tr1.css("height", newHeight + 1);
$tr2.css("height", newHeight + 1);
},
_adjustWrapperLayout: function () {
var $wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames,
$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
$topRightWrapper = $wrapper.find("div." + fcn.topRight),
$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft),
$bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight),
$topLeftTable = $topLeftWrapper.find("table"),
$topRightTable = $topRightWrapper.find("table"),
// $bottomLeftTable = $bottomLeftWrapper.find("table"),
wrapperWidth = $wrapper.width(),
wrapperHeight = $wrapper.height(),
fixedWidth = Math.min(wrapperWidth, $topLeftTable.width()),
fixedHeight = Math.min(
wrapperHeight,
Math.max($topLeftTable.height(), $topRightTable.height())
);
// vScrollbar = $bottomRightWrapper.get(0).scrollHeight > (wrapperHeight - fixedHeight),
// hScrollbar = $bottomRightWrapper.get(0).scrollWidth > (wrapperWidth - fixedWidth);
$topLeftWrapper.css({
width: fixedWidth,
height: fixedHeight,
});
$topRightWrapper.css({
// width: wrapperWidth - fixedWidth - (vScrollbar ? 17 : 0),
// width: "calc(100% - " + (fixedWidth + (vScrollbar ? 17 : 0)) + "px)",
width: "calc(100% - " + (fixedWidth + 17) + "px)",
height: fixedHeight,
left: fixedWidth,
});
$bottomLeftWrapper.css({
width: fixedWidth,
// height: vScrollbar ? wrapperHeight - fixedHeight - (hScrollbar ? 17 : 0) : "auto",
// height: vScrollbar ? ("calc(100% - " + (fixedHeight + (hScrollbar ? 17 : 0)) + "px)") : "auto",
// height: vScrollbar ? ("calc(100% - " + (fixedHeight + 17) + "px)") : "auto",
height: "calc(100% - " + (fixedHeight + 17) + "px)",
top: fixedHeight,
});
$bottomRightWrapper.css({
// width: wrapperWidth - fixedWidth,
// height: vScrollbar ? wrapperHeight - fixedHeight : "auto",
width: "calc(100% - " + fixedWidth + "px)",
// height: vScrollbar ? ("calc(100% - " + fixedHeight + "px)") : "auto",
height: "calc(100% - " + fixedHeight + "px)",
top: fixedHeight,
left: fixedWidth,
});
},
_adjustLayout: function () {
var self = this,
$wrapper = this.$fixedWrapper,
fcn = this.options.fixed.classNames,
$topLeftWrapper = $wrapper.find("div." + fcn.topLeft),
$topRightWrapper = $wrapper.find("div." + fcn.topRight),
$bottomLeftWrapper = $wrapper.find("div." + fcn.bottomLeft);
// $bottomRightWrapper = $wrapper.find("div." + fcn.bottomRight)
$topLeftWrapper.find("table tr").each(function (idx) {
var $trRight = $topRightWrapper.find("tr").eq(idx);
self.ext.fixed._adjustRowHeight($(this), $trRight);
});
$bottomLeftWrapper
.find("table tbody")
.find("tr")
.each(function (idx) {
// var $trRight = $bottomRightWrapper.find("tbody").find("tr").eq(idx);
self.ext.fixed._adjustRowHeight($(this));
});
self.ext.fixed._adjustColWidths.call(this);
self.ext.fixed._adjustWrapperLayout.call(this);
},
// treeSetFocus: function(ctx, flag) {
//// alert("treeSetFocus" + ctx.tree.$container);
// ctx.tree.$container.trigger("focus");
// $.ui.fancytree.focusTree = ctx.tree;
// }
});
// Value returned by `require('jquery.fancytree..')`
return $.ui.fancytree;
}); // End of closure