All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.primefaces.tree.tree.base.js Maven / Gradle / Ivy

There is a newer version: 14.0.0
Show newest version
/**
 * __PrimeFaces Base Tree Widget__
 * 
 * A tree is used for displaying hierarchical data and creating a site navigation.
 * 
 * @typedef {"self" | "parent" | "ancestor"} PrimeFaces.widget.BaseTree.DragMode Drag mode for a tree widget. Defines
 * the parent-child relationship when a node is dragged.
 * 
 * @typedef {"lenient" | "strict"} PrimeFaces.widget.BaseTree.FilterMode Mode for filtering a tree widget.
 * 
 * @typedef {"single" | "multiple" | "checkbox"} PrimeFaces.widget.BaseTree.SelectionMode How the nodes of a tree are
 * selected. When set to `single`, only at most one node can be selected by clicking on it. When set to `multiple`,
 * more than one node may be selected by clicking on each node. When set to `checkbox`, each node receives a checkbox
 * next to it that may be used for selection.
 * 
 * @typedef PrimeFaces.widget.BaseTree.OnNodeClickCallback Callback that is invoked when a node is clicked, see
 * {@link BaseTreeCfg.onNodeClick}.
 * @this {PrimeFaces.widget.BaseTree} PrimeFaces.widget.BaseTree.OnNodeClickCallback
 * @param {JQuery} PrimeFaces.widget.BaseTree.OnNodeClickCallback.node The tree node that was clicked.
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.BaseTree.OnNodeClickCallback.event The mouse click event that occurred.
 * @return {boolean} PrimeFaces.widget.BaseTree.OnNodeClickCallback `true` to allow the node to be selected, `false` to
 * ignore the click.
 * 
 * @interface {PrimeFaces.widget.BaseTree.NodeIconSet} NodeIconSet A set of icons to be used for a certain node type.
 * @prop {string} NodeIconSet.expandedIcon Icon to be used when the node is expanded.
 * @prop {string} NodeIconSet.collapsedIcon Icon to be used when the node is collapsed.
 * 
 * @implements {PrimeFaces.widget.ContextMenu.ContextMenuProvider}
 * 
 * @prop {JQuery} cursorNode When multiple nodes are selected, the selected node on which the user clicked most
 * recently.
 * @prop {JQuery|null} focusedNode DOM element of the node which is currently focused, if any.
 * @prop {Document | string} [jqTargetId] Target of the context menu, when a context menu is used.
 * @prop {string} selections List of nodes which are currently selected. Each item is the row key of a selected node.
 * @prop {JQuery} selectionHolder DOM element of the hidden form element that holds the list of selected nodes.
 * 
 * @interface {PrimeFaces.widget.BaseTreeCfg} cfg The configuration for the {@link  BaseTree| BaseTree widget}.
 * You can access this configuration via {@link PrimeFaces.widget.BaseWidget.cfg|BaseWidget.cfg}. Please note that this
 * configuration is usually meant to be read-only and should not be modified.
 * @extends {PrimeFaces.widget.BaseWidgetCfg} cfg 
 * 
 * @prop {boolean} cfg.animate `true` is the tree is animated, or `false` otherwise.
 * @prop {boolean} cfg.cache `true` if the content of dynamically loaded nodes is cached for the next time the node is
 * expanded, or `false` to always fetch the content from the server.
 * @prop {boolean} cfg.disabled `true` is this widget is disabled, or `false` otherwise.
 * @prop {string} cfg.dragdropScope Optional scope for the dragging and dropping, passed to JQuery UI.
 * @prop {boolean} cfg.draggable `true` if nodes are draggable, or `false` otherwise.
 * @prop {PrimeFaces.widget.BaseTree.DragMode} cfg.dragMode Defines parent-child relationship when a node is dragged.
 * @prop {boolean} cfg.dropCopyNode When enabled, the copy of the selected nodes can be dropped from a tree to another
 * tree using Shift key.
 * @prop {boolean} cfg.droppable `true` if nodes are droppable, or `false` otherwise.
 * @prop {boolean} cfg.dynamic `true` if the content of nodes is loaded dynamically as needed, or `false` otherwise.
 * @prop {string} cfg.event Event for the context menu.
 * @prop {boolean} cfg.filter `true` if filtering is enabeld, `false` otherwise.
 * @prop {PrimeFaces.widget.BaseTree.FilterMode} cfg.filterMode Mode for filtering.
 * @prop {boolean} cfg.highlight `true` if selected nodes are highlighted, or `false` otherwise.
 * @prop {Record} cfg.iconStates A map between the type of a node and the
 * icons for that node.
 * @prop {boolean} cfg.multipleDrag When enabled, the selected multiple nodes can be dragged from a tree to another
 * tree.
 * @prop {string} cfg.nodeType Node type of nodes for which the context menu is available.
 * @prop {PrimeFaces.widget.BaseTree.OnNodeClickCallback} cfg.onNodeClick Callback that is invoked when a node is
 * clicked. If it returns `false`, the click on the node is ignored.
 * @prop {boolean} cfg.propagateDown Whether toggling a node checkbox is propagated downwards.
 * @prop {boolean} cfg.propagateUp Whether toggling a node checkbox is propagated upwards.
 * @prop {PrimeFaces.widget.BaseTree.SelectionMode} cfg.selectionMode How the node of this tree can be selected, if
 * selection is enabled at all.
 */
PrimeFaces.widget.BaseTree = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.cfg.highlight = (this.cfg.highlight === false) ? false : true;
        this.focusedNode = null;

        if(!this.cfg.disabled) {
            if(this.cfg.selectionMode) {
                this.initSelection();
            }

            this.bindEvents();

            this.jq.data('widget', cfg.widgetVar);
        }
    },

    /**
     * Called when this tree is initialized. Performs any setup required for enabling the selection of node.
     * @protected
     */
    initSelection: function() {
        this.selectionHolder = $(this.jqId + '_selection');
        var selectionsValue = this.selectionHolder.val();
        this.selections = selectionsValue === '' ? [] : selectionsValue.split(',');

        if(this.cursorNode) {
            this.cursorNode = this.jq.find('.ui-treenode[data-rowkey="' + $.escapeSelector(this.cursorNode.data('rowkey')) + '"]');
        }

        if(this.isCheckboxSelection() && this.cfg.propagateUp) {
            this.preselectCheckbox();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.widget.ContextMenu} menuWidget
     * @param {PrimeFaces.widget.BaseTree} targetWidget
     * @param {string} targetId
     * @param {PrimeFaces.widget.ContextMenuCfg} cfg 
     */
    bindContextMenu : function(menuWidget, targetWidget, targetId, cfg) {
        var nodeContentSelector = targetId + ' .ui-tree-selectable',
        nodeEvent = cfg.nodeType ? cfg.event + '.treenode.' + cfg.nodeType : cfg.event + '.treenode',
        containerEvent = cfg.event + '.tree';

        $(document).off(nodeEvent, nodeContentSelector).on(nodeEvent, nodeContentSelector, null, function(e) {
            var nodeContent = $(this);

            if($(e.target).is(':not(.ui-tree-toggler)') && (cfg.nodeType === undefined || nodeContent.parent().data('nodetype') === cfg.nodeType)) {
                var isContextMenuDelayed = targetWidget.nodeRightClick(e, nodeContent, function(){
                    menuWidget.show(e);
                });

                if(isContextMenuDelayed) {
                    e.preventDefault();
                    e.stopPropagation(); 
                }
            }
        });

        $(document).off(containerEvent, this.jqTargetId).on(containerEvent, this.jqTargetId, null, function(e) {
            if(e.target.id == targetWidget.id && targetWidget.isEmpty()) {
                menuWidget.show(e);
            }
        });
    },

    /**
     * Expands the given node, as if the user had clicked on the `+` icon of the node. The children of the node will now
     * be visible. 
     * @param {JQuery} node Node to expand. 
     */
    expandNode: function(node) {
        var $this = this;

        if(this.cfg.dynamic) {
            if(this.cfg.cache && $this.getNodeChildrenContainer(node).children().length > 0) {
                this.showNodeChildren(node);

                return;
            }

            if(node.data('processing')) {
                PrimeFaces.debug('Node is already being expanded, ignoring expand event.');
                return;
            }

            node.data('processing', true);

            var options = {
                source: this.id,
                process: this.id,
                update: this.id,
                formId: this.getParentFormId(),
                params: [
                    {name: this.id + '_expandNode', value: $this.getRowKey(node)}
                ],
                onsuccess: function(responseXML, status, xhr) {
                    PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                            widget: $this,
                            handle: function(content) {
                                var nodeChildrenContainer = this.getNodeChildrenContainer(node);
                                nodeChildrenContainer.append(content);

                                this.showNodeChildren(node);

                                if(this.cfg.draggable) {
                                    this.makeDraggable(nodeChildrenContainer.find('.ui-treenode-content'));
                                }

                                if(this.cfg.droppable) {
                                    this.makeDropPoints(nodeChildrenContainer.find('li.ui-tree-droppoint'));
                                    this.makeDropNodes(nodeChildrenContainer.find('div.ui-treenode-droppable'));
                                }
                            }
                        });

                    return true;
                },
                oncomplete: function() {
                    node.removeData('processing');
                }
            };

            if(this.hasBehavior('expand')) {
                this.callBehavior('expand', options);
            }
            else {
                PrimeFaces.ajax.Request.handle(options);
            }
        }
        else {
            this.showNodeChildren(node);
            this.fireExpandEvent(node);
        }
    },

    /**
     * Called when a node was expanded. Fire the appropriate event.
     * @protected
     * @param {JQuery} node The node for which to fire the event.
     */
    fireExpandEvent: function(node) {
        if(this.hasBehavior('expand')) {
            var ext = {
                params: [
                    {name: this.id + '_expandNode', value: this.getRowKey(node)}
                ]
            };

            this.callBehavior('expand', ext);
        }
    },

    /**
     * Called when a node was collapsed. Fire the appropriate event.
     * @protected
     * @param {JQuery} node The node for which to fire the event.
     */
    fireCollapseEvent: function(node) {
        if(this.hasBehavior('collapse')) {
            var ext = {
                params: [
                    {name: this.id + '_collapseNode', value: this.getRowKey(node)}
                ]
            };

            this.callBehavior('collapse', ext);
        }
    },

    /**
     * Finds the DOM element for the container which contains the child nodes of the given node.
     * @protected
     * @param {JQuery} node A node for which to get the children container.
     * @return {JQuery} The container with the children of the given node.
     */
    getNodeChildrenContainer: function(node) {
        throw "Unsupported Operation";
    },

    /**
     * Makes the children of the given node visible. Called when a node is expanded.
     * @protected
     * @param {JQuery} node Node with children to display.
     */
    showNodeChildren: function(node) {
        throw "Unsupported Operation";
    },

    /**
     * Saves the list of currently selected nodes in a hidden form element.
     * @protected
     */
    writeSelections: function() {
        this.selectionHolder.val(this.selections.join(','));
    },

    /**
     * Called when a node was selected. Fire the appropriate event.
     * @protected
     * @param {JQuery} node The node for which to fire the event.
     */
    fireNodeSelectEvent: function(node) {
        if(this.isCheckboxSelection() && this.cfg.dynamic) {
            var $this = this,
            options = {
                source: this.id,
                process: this.id
            };

            options.params = [
                {name: this.id + '_instantSelection', value: this.getRowKey(node)}
            ];

            options.oncomplete = function(xhr, status, args, data) {
                if(args.descendantRowKeys && args.descendantRowKeys !== '') {
                    var rowKeys = args.descendantRowKeys.split(',');
                    for(var i = 0; i < rowKeys.length; i++) {
                        $this.addToSelection(rowKeys[i]);
                    }
                    $this.writeSelections();
                }
            };

            if(this.hasBehavior('select')) {
                this.callBehavior('select', options);
            }
            else {
                PrimeFaces.ajax.Request.handle(options);
            }
        }
        else {
            if(this.hasBehavior('select')) {
                var ext = {
                    params: [
                        {name: this.id + '_instantSelection', value: this.getRowKey(node)}
                    ]
                };

                this.callBehavior('select', ext);
            }
        }
    },

    /**
     * Called when a node was unselected. Fire the appropriate event.
     * @protected
     * @param {JQuery} node The node for which to fire the event.
     */
    fireNodeUnselectEvent: function(node) {
        if(this.hasBehavior('unselect')) {
            var ext = {
                params: [
                    {name: this.id + '_instantUnselection', value: this.getRowKey(node)}
                ]
            };

            this.callBehavior('unselect', ext);
        }
    },

    /**
     * Called when a right click was performed on a node. Fire the appropriate event.
     * @protected
     * @param {JQuery} node The node for which to fire the event.
     * @param {() => void} fnShowMenu Callback that is invoked once the context menu is shown.
     */
    fireContextMenuEvent: function(node, fnShowMenu) {
        if(this.hasBehavior('contextMenu')) {
            var ext = {
                params: [
                    {name: this.id + '_contextMenuNode', value: this.getRowKey(node)}
                ],
                oncomplete: function() {
                    fnShowMenu();
                }
            };

            this.callBehavior('contextMenu', ext);
        } else {
            fnShowMenu();
        }
    },

    /**
     * Finds the row key (unique ID) of the given node.
     * @param {JQuery} node A node for which to find the row key.
     * @return {string} The key of the given node.
     */
    getRowKey: function(node) {
        return node.attr('data-rowkey');
    },

    /**
     * Checks whether the given node is currently selected, irrespective of the current selection mode.
     * @param {JQuery} node A node to check.
     * @return {boolean} `true` if the given node is selected, or `false` otherwise.
     */
    isNodeSelected: function(node) {
        return $.inArray(this.getRowKey(node), this.selections) != -1;
    },

    /**
     * Checks whether the selection mode of this tree is set to `single`.
     * @return {boolean} `true` if the current selection mode is `single`, or `false` otherwise.
     */
    isSingleSelection: function() {
        return this.cfg.selectionMode == 'single';
    },

    /**
     * Checks whether the selection mode of this tree is set to `multiple`.
     * @return {boolean} `true` if the current selection mode is `multiple`, or `false` otherwise.
     */
    isMultipleSelection: function() {
        return this.cfg.selectionMode == 'multiple';
    },

    /**
     * Checks whether the selection mode of this tree is set to `checkbox`.
     * @return {boolean} `true` if the current selection mode is `checkbox`, or `false` otherwise.
     */
    isCheckboxSelection: function() {
        return this.cfg.selectionMode == 'checkbox';
    },

    /**
     * Adds the given node to the list of selected nodes.
     * @protected
     * @param {string} rowKey Row key of the node to add to the selected nodes.
     */
    addToSelection: function(rowKey) {
        if(!PrimeFaces.inArray(this.selections, rowKey)) {
            this.selections.push(rowKey);
        }
    },

    /**
     * Removes the given node from the list of currently selected nodes.
     * @protected
     * @param {string} rowKey Row key of a node to to remove from the current selection.
     */
    removeFromSelection: function(rowKey) {
        this.selections = $.grep(this.selections, function(r) {
            return r !== rowKey;
        });
    },

    /**
     * Removes all chilren of the given node from the list of currently selected nodes.
     * @protected
     * @param {string} rowKey Row key of a node to process.
     */
    removeDescendantsFromSelection: function(rowKey) {
        var newSelections = [];
        for(var i = 0; i < this.selections.length; i++) {
            if(this.selections[i].indexOf(rowKey + '_') !== 0)
                newSelections.push(this.selections[i]);
        }
        this.selections = newSelections;
    },

    /**
     * Invoked in response to a normal click on a node.
     * @protected
     * @param {JQuery.TriggeredEvent} event Event of the click.
     * @param {JQuery} nodeContent Content of the clicked node.
     */
    nodeClick: function(event, nodeContent) {
        if($(event.target).is(':not(.ui-tree-toggler)')) {
            var node = nodeContent.parent(),
            selectable = nodeContent.hasClass('ui-tree-selectable');

            if(this.cfg.onNodeClick) {
                var retVal = this.cfg.onNodeClick.call(this, node, event);
                if (retVal === false) {
                    return;
                }
            }

            if(selectable && this.cfg.selectionMode) {
                var selected = this.isNodeSelected(node),
                metaKey = event.metaKey||event.ctrlKey,
                shiftKey = event.shiftKey;

                if(this.isCheckboxSelection()) {
                    this.toggleCheckboxNode(node);
                }
                else {
                    if(selected && (metaKey)) {
                        this.unselectNode(node);
                    }
                    else {
                        if(this.isSingleSelection()||(this.isMultipleSelection() && !metaKey)) {
                            this.unselectAllNodes();
                        }

                        if(this.isMultipleSelection() && shiftKey && this.cursorNode && (this.cursorNode.parent().is(node.parent()))) {
                            var parentList = node.parent(),
                            treenodes = parentList.children('li.ui-treenode'),
                            currentNodeIndex = treenodes.index(node),
                            cursorNodeIndex = treenodes.index(this.cursorNode),
                            startIndex = (currentNodeIndex > cursorNodeIndex) ? cursorNodeIndex : currentNodeIndex,
                            endIndex = (currentNodeIndex > cursorNodeIndex) ? (currentNodeIndex + 1) : (cursorNodeIndex + 1);

                            for(var i = startIndex; i < endIndex; i++) {
                                var treenode = treenodes.eq(i);
                                if(treenode.is(':visible')) {
                                    if(i === (endIndex - 1))
                                        this.selectNode(treenode);
                                    else
                                        this.selectNode(treenode, true);
                                }
                            }
                        }
                        else {
                            this.selectNode(node);
                            this.cursorNode = node;
                        }
                    }
                }

                if($(event.target).is(':not(:input:enabled)')) {
                    PrimeFaces.clearSelection();
                    this.focusNode(node);
                }
            }
        }
    },

    /**
     * Invoked in response to a right click on a node.
     * @protected
     * @param {JQuery.TriggeredEvent} event Event of the right click.
     * @param {JQuery} nodeContent Content of the clicked node.
     * @param {() => void} fnShowMenu Callback that is invoked when the context menu is shown. 
     * @return {boolean} `true` if the context menu was opened, or `false` otherwise.
     */
    nodeRightClick: function(event, nodeContent, fnShowMenu) {
        PrimeFaces.clearSelection();

        if($(event.target).is(':not(.ui-tree-toggler)')) {
            var node = nodeContent.parent(),
            selectable = nodeContent.hasClass('ui-tree-selectable');

            if(selectable && this.cfg.selectionMode) {
                var selected = this.isNodeSelected(node);
                if(!selected) {
                    if(this.isCheckboxSelection()) {
                        this.toggleCheckboxNode(node);
                    }
                    else {
                        this.unselectAllNodes();
                        this.selectNode(node, true);
                        this.cursorNode = node;
                    }
                }

                this.fireContextMenuEvent(node, fnShowMenu);
                return true;
            }
        }
        return false;
    },

    /**
     * A sub class may perform any setup related to registering event handlers in this method, such as listening to
     * mouse clicks or keyboard presses.
     * @protected
     */
    bindEvents: function() {
        throw "Unsupported Operation";
    },

    /**
     * This method must select the given node. When `silent` is set to `true`, no events should be triggered in response
     * to this action.
     * @param {JQuery} node A node of this tree to select.
     * @param {boolean} [silent] `true` if no events should be triggered, or `false` otherwise. 
     */
    selectNode: function(node, silent) {
        throw "Unsupported Operation";
    },

    /**
     * This method must unselect the given node. When `silent` is set to `true`, no events should be triggered in
     * response to this action.
     * @param {JQuery} node A node of this tree to unselect.
     * @param {boolean} [silent] `true` if no events should be triggered, or `false` otherwise. 
     */
    unselectNode: function(node, silent) {
        throw "Unsupported Operation";
    },

    /**
     * This method must unselect all nodes of this tree that are selected.
     */
    unselectAllNodes: function() {
        throw "Unsupported Operation";
    },

    /**
     * Called once during widget initialization if this tree has got nodes with selectable checkboxes.
     * @protected
     */
    preselectCheckbox: function() {
        throw "Unsupported Operation";
    },

    /**
     * Called when the nodes of this tree are selected via checkboxes. Must select the checkbox of the given node.
     * @protected
     * @param {JQuery} node Node with a checkbox to toggle.
     */
    toggleCheckboxNode: function(node) {
        throw "Unsupported Operation";
    },

    /**
     * Checks whether this tree is empty, that is, whether it contains any nodes.
     * @return {boolean} `true` if this tree has got no nodes, or `false` otherwise.
     */
    isEmpty: function() {
        throw "Unsupported Operation";
    },

    /**
     * When this tree has got selectable nodes with checkboxes, checks or unchecks the given checkbox.
     * @param {JQuery} checkbox A checkbox of a node to check or uncheck.
     * @param {boolean} checked `true` to check the given node, `false` to uncheck it.
     */
    toggleCheckboxState: function(checkbox, checked) {
        if(checked)
            this.uncheck(checkbox);
        else
            this.check(checkbox);
    },

    /**
     * When this tree has got selectable nodes with checkboxes, partially selects the given checkbox. Does nothing
     * otherwise.
     * @protected
     * @param {JQuery} checkbox Checkbox of a node to check partially.
     */
    partialCheck: function(checkbox) {
        var box = checkbox.children('.ui-chkbox-box'),
        icon = box.children('.ui-chkbox-icon'),
        treeNode = checkbox.closest('.ui-treenode'),
        rowKey = this.getRowKey(treeNode);

        box.removeClass('ui-state-active');
        treeNode.find('> .ui-treenode-content').removeClass('ui-state-highlight')
                .find('> .ui-treenode-label').removeClass('ui-state-highlight');
        icon.removeClass('ui-icon-blank ui-icon-check').addClass('ui-icon-minus');
        treeNode.removeClass('ui-treenode-selected ui-treenode-unselected').addClass('ui-treenode-hasselected').attr('aria-checked', false).attr('aria-selected', false);

        this.removeFromSelection(rowKey);
    },

    /**
     * When this tree has got selectable nodes with checkboxes, selects the given checkbox. Does nothing otherwise.
     * @protected
     * @param {JQuery} checkbox Checkbox of a node to check.
     */
    check: function(checkbox) {
        var box = checkbox.children('.ui-chkbox-box'),
        icon = box.children('.ui-chkbox-icon'),
        treeNode = checkbox.closest('.ui-treenode'),
        rowKey = this.getRowKey(treeNode);

        box.addClass('ui-state-active');
        icon.removeClass('ui-icon-blank ui-icon-minus').addClass('ui-icon-check');
        treeNode.removeClass('ui-treenode-hasselected ui-treenode-unselected').addClass('ui-treenode-selected').attr('aria-checked', true).attr('aria-selected', true);

        this.addToSelection(rowKey);
    },

    /**
     * When this tree has got selectable nodes with checkboxes, unselects the given checkbox. Does nothing otherwise.
     * @protected
     * @param {JQuery} checkbox Checkbox of a node to uncheck.
     */
    uncheck: function(checkbox) {
        var box = checkbox.children('.ui-chkbox-box'),
        icon = box.children('.ui-chkbox-icon'),
        treeNode = checkbox.closest('.ui-treenode'),
        rowKey = this.getRowKey(treeNode);

        box.removeClass('ui-state-active');
        icon.removeClass('ui-icon-minus ui-icon-check').addClass('ui-icon-blank');
        treeNode.removeClass('ui-treenode-hasselected ui-treenode-selected').addClass('ui-treenode-unselected').attr('aria-checked', false).attr('aria-selected', false);

        this.removeFromSelection(rowKey);
    },

    /**
     * Checks whether the given node is currently expanded, that is, whether its children are visible.
     * @param {JQuery} node Node to check. 
     * @return {boolean} `true` if the node is expanded, or `false` otherwise.
     */
    isExpanded: function(node) {
        return this.getNodeChildrenContainer(node).is(':visible');
    },

    /**
     * Puts focus on the given node.
     * @protected
     * @param {JQuery} node A node on which to put focus.
     */
    focusNode: function(node) {
        throw "Unsupported Operation";
    }

});




© 2015 - 2024 Weber Informatics LLC | Privacy Policy