mar10/fancytree

View on GitHub
src/jquery.fancytree.fixed.js

Summary

Maintainability
F
6 days
Test Coverage
/*!
 * 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