mar10/fancytree

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

Summary

Maintainability
F
4 days
Test Coverage
/*!
 * jquery.fancytree.table.js
 *
 * Render tree as table (aka 'tree grid', 'table tree').
 * (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
 */

(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 _assert = $.ui.fancytree.assert;

    function insertFirstChild(referenceNode, newNode) {
        referenceNode.insertBefore(newNode, referenceNode.firstChild);
    }

    function insertSiblingAfter(referenceNode, newNode) {
        referenceNode.parentNode.insertBefore(
            newNode,
            referenceNode.nextSibling
        );
    }

    /* Show/hide all rows that are structural descendants of `parent`. */
    function setChildRowVisibility(parent, flag) {
        parent.visit(function (node) {
            var tr = node.tr;
            // currentFlag = node.hide ? false : flag; // fix for ext-filter
            if (tr) {
                tr.style.display = node.hide || !flag ? "none" : "";
            }
            if (!node.expanded) {
                return "skip";
            }
        });
    }

    /* Find node that is rendered in previous row. */
    function findPrevRowNode(node) {
        var i,
            last,
            prev,
            parent = node.parent,
            siblings = parent ? parent.children : null;

        if (siblings && siblings.length > 1 && siblings[0] !== node) {
            // use the lowest descendant of the preceeding sibling
            i = $.inArray(node, siblings);
            prev = siblings[i - 1];
            _assert(prev.tr, "prev.tr missing: " + prev);
            // descend to lowest child (with a <tr> tag)
            while (prev.children && prev.children.length) {
                last = prev.children[prev.children.length - 1];
                if (!last.tr) {
                    break;
                }
                prev = last;
            }
        } else {
            // if there is no preceding sibling, use the direct parent
            prev = parent;
        }
        return prev;
    }

    $.ui.fancytree.registerExtension({
        name: "table",
        version: "@VERSION",
        // 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,
                n,
                $row,
                $tbody,
                tree = ctx.tree,
                opts = ctx.options,
                tableOpts = opts.table,
                $table = tree.widget.element;

            if (tableOpts.customStatus != null) {
                if (opts.renderStatusColumns == null) {
                    tree.warn(
                        "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' instead."
                    );
                    opts.renderStatusColumns = tableOpts.customStatus;
                } else {
                    $.error(
                        "The 'customStatus' option is deprecated since v2.15.0. Use 'renderStatusColumns' only instead."
                    );
                }
            }
            if (opts.renderStatusColumns) {
                if (opts.renderStatusColumns === true) {
                    opts.renderStatusColumns = opts.renderColumns;
                    // } else if( opts.renderStatusColumns === "wide" ) {
                    //     opts.renderStatusColumns = _renderStatusNodeWide;
                }
            }

            $table.addClass("fancytree-container 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
            tree.columnCount = $("thead >tr", $table)
                .last()
                .find(">th", $table).length;
            // Read TR templates from tbody if any
            $row = $tbody.children("tr").first();
            if ($row.length) {
                n = $row.children("td").length;
                if (tree.columnCount && n !== tree.columnCount) {
                    tree.warn(
                        "Column count mismatch between thead (" +
                            tree.columnCount +
                            ") and tbody (" +
                            n +
                            "): using tbody."
                    );
                    tree.columnCount = n;
                }
                $row = $row.clone();
            } else {
                // Only thead is defined: create default row markup
                _assert(
                    tree.columnCount >= 1,
                    "Need either <thead> or <tbody> with <td> elements to determine column count."
                );
                $row = $("<tr />");
                for (i = 0; i < tree.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));

            // // If tbody contains a second row, use this as status node template
            // $row = $tbody.children("tr").eq(1);
            // if( $row.length === 0 ) {
            //     tree.statusRowFragment = tree.rowFragment;
            // } else {
            //     $row = $row.clone();
            //     tree.statusRowFragment = document.createDocumentFragment();
            //     tree.statusRowFragment.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-table
            tree.$container = $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);
            }
        },
        nodeRemoveChildMarkup: function (ctx) {
            var node = ctx.node;
            //        node.debug("nodeRemoveChildMarkup()");
            node.visit(function (n) {
                if (n.tr) {
                    $(n.tr).remove();
                    n.tr = null;
                }
            });
        },
        nodeRemoveMarkup: function (ctx) {
            var node = ctx.node;
            //        node.debug("nodeRemoveMarkup()");
            if (node.tr) {
                $(node.tr).remove();
                node.tr = null;
            }
            this.nodeRemoveChildMarkup(ctx);
        },
        /* Override standard render. */
        nodeRender: function (ctx, force, deep, collapsed, _recursive) {
            var children,
                firstTr,
                i,
                l,
                newRow,
                prevNode,
                prevTr,
                subCtx,
                tree = ctx.tree,
                node = ctx.node,
                opts = ctx.options,
                isRootNode = !node.parent;

            if (tree._enableUpdate === false) {
                // $.ui.fancytree.debug("*** nodeRender _enableUpdate: false");
                return;
            }
            if (!_recursive) {
                ctx.hasCollapsedParents = node.parent && !node.parent.expanded;
            }
            // $.ui.fancytree.debug("*** nodeRender " + node + ", isRoot=" + isRootNode, "tr=" + node.tr, "hcp=" + ctx.hasCollapsedParents, "parent.tr=" + (node.parent && node.parent.tr));
            if (!isRootNode) {
                if (node.tr && force) {
                    this.nodeRemoveMarkup(ctx);
                }
                if (node.tr) {
                    if (force) {
                        // Set icon, link, and title (normally this is only required on initial render)
                        this.nodeRenderTitle(ctx); // triggers renderColumns()
                    } else {
                        // Update element classes according to node state
                        this.nodeRenderStatus(ctx);
                    }
                } else {
                    if (ctx.hasCollapsedParents && !deep) {
                        // #166: we assume that the parent will be (recursively) rendered
                        // later anyway.
                        // node.debug("nodeRender ignored due to unrendered parent");
                        return;
                    }
                    // Create new <tr> after previous row
                    // if( node.isStatusNode() ) {
                    //     newRow = tree.statusRowFragment.firstChild.cloneNode(true);
                    // } else {
                    newRow = tree.rowFragment.firstChild.cloneNode(true);
                    // }
                    prevNode = findPrevRowNode(node);
                    // $.ui.fancytree.debug("*** nodeRender " + node + ": prev: " + prevNode.key);
                    _assert(prevNode);
                    if (collapsed === true && _recursive) {
                        // hide all child rows, so we can use an animation to show it later
                        newRow.style.display = "none";
                    } else if (deep && ctx.hasCollapsedParents) {
                        // also hide this row if deep === true but any parent is collapsed
                        newRow.style.display = "none";
                        //                    newRow.style.color = "red";
                    }
                    if (prevNode.tr) {
                        insertSiblingAfter(prevNode.tr, newRow);
                    } else {
                        _assert(
                            !prevNode.parent,
                            "prev. row must have a tr, or be system root: " +
                                prevNode
                        );
                        // tree.tbody.appendChild(newRow);
                        insertFirstChild(tree.tbody, newRow); // #675
                    }
                    node.tr = newRow;
                    if (node.key && opts.generateIds) {
                        node.tr.id = opts.idPrefix + node.key;
                    }
                    node.tr.ftnode = node;
                    // if(opts.aria){
                    //     $(node.tr).attr("aria-labelledby", "ftal_" + 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)
                    this.nodeRenderTitle(ctx);
                    // Allow tweaking, binding, after node was created for the first time
                    //                tree._triggerNodeEvent("createNode", ctx);
                    if (opts.createNode) {
                        opts.createNode.call(tree, { type: "createNode" }, ctx);
                    }
                }
            }
            // Allow tweaking after node state was rendered
            //        tree._triggerNodeEvent("renderNode", ctx);
            if (opts.renderNode) {
                opts.renderNode.call(tree, { type: "renderNode" }, ctx);
            }
            // Visit child nodes
            // Add child markup
            children = node.children;
            if (children && (isRootNode || deep || node.expanded)) {
                for (i = 0, l = children.length; i < l; i++) {
                    subCtx = $.extend({}, ctx, { node: children[i] });
                    subCtx.hasCollapsedParents =
                        subCtx.hasCollapsedParents || !node.expanded;
                    this.nodeRender(subCtx, force, deep, collapsed, true);
                }
            }
            // Make sure, that <tr> order matches node.children order.
            if (children && !_recursive) {
                // we only have to do it once, for the root branch
                prevTr = node.tr || null;
                firstTr = tree.tbody.firstChild;
                // Iterate over all descendants
                node.visit(function (n) {
                    if (n.tr) {
                        if (
                            !n.parent.expanded &&
                            n.tr.style.display !== "none"
                        ) {
                            // fix after a node was dropped over a collapsed
                            n.tr.style.display = "none";
                            setChildRowVisibility(n, false);
                        }
                        if (n.tr.previousSibling !== prevTr) {
                            node.debug("_fixOrder: mismatch at node: " + n);
                            var nextTr = prevTr ? prevTr.nextSibling : firstTr;
                            tree.tbody.insertBefore(n.tr, nextTr);
                        }
                        prevTr = n.tr;
                    }
                });
            }
            // Update element classes according to node state
            // if(!isRootNode){
            //     this.nodeRenderStatus(ctx);
            // }
        },
        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.table.mergeStatusColumns && node.isTopLevel()) {
                    $(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) {
            // flag defaults to true
            flag = flag !== false;

            if ((ctx.node.expanded && flag) || (!ctx.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, args) {
                // ctx.tree.info("ok:" + ok, args);
                if (ok) {
                    // #1108 minExpandLevel: 2 together with table extension does not work
                    // don't call when 'ok' is false:
                    setChildRowVisibility(ctx.node, flag);
                    if (
                        flag &&
                        ctx.options.autoScroll &&
                        !callOpts.noAnimation &&
                        ctx.node.hasChildren()
                    ) {
                        // Scroll down to last child, but keep current node visible
                        ctx.node
                            .getLastChild()
                            .scrollIntoView(true, { topNode: ctx.node })
                            .always(function () {
                                if (!callOpts.noEvents) {
                                    ctx.tree._triggerNodeEvent(
                                        flag ? "expand" : "collapse",
                                        ctx
                                    );
                                }
                                dfd.resolveWith(ctx.node);
                            });
                    } else {
                        if (!callOpts.noEvents) {
                            ctx.tree._triggerNodeEvent(
                                flag ? "expand" : "collapse",
                                ctx
                            );
                        }
                        dfd.resolveWith(ctx.node);
                    }
                } else {
                    if (!callOpts.noEvents) {
                        ctx.tree._triggerNodeEvent(
                            flag ? "expand" : "collapse",
                            ctx
                        );
                    }
                    dfd.rejectWith(ctx.node);
                }
            }
            // Call base-expand with disabled events and animation
            this._super(ctx, flag, subOpts)
                .done(function () {
                    _afterExpand(true, arguments);
                })
                .fail(function () {
                    _afterExpand(false, arguments);
                });
            return dfd.promise();
        },
        nodeSetStatus: function (ctx, status, message, details) {
            if (status === "ok") {
                var node = ctx.node,
                    firstChild = node.children ? node.children[0] : null;
                if (firstChild && firstChild.isStatusNode()) {
                    $(firstChild.tr).remove();
                }
            }
            return this._superApply(arguments);
        },
        treeClear: function (ctx) {
            this.nodeRemoveChildMarkup(this._makeHookContext(this.rootNode));
            return this._superApply(arguments);
        },
        treeDestroy: function (ctx) {
            this.$container.find("tbody").empty();
            if (this.$source) {
                this.$source.removeClass("fancytree-helper-hidden");
            }
            return this._superApply(arguments);
        },
        /*,
    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