if (!PrimeFaces.dialog) {

    /**
     * The object with functionality related to working with dialogs and the dialog framework.
     * @namespace
     * 
     * @interface {PrimeFaces.dialog.DialogHandlerCfg} DialogHandlerCfg Interface of the configuration object for a
     * dialog of the dialog framework. Used by `PrimeFaces.dialog.DialogHandler.openDialog`.
     * @prop {Partial<PrimeFaces.dialog.DialogHandlerCfgOptions>} DialogHandlerCfg.options The options for the dialog.
     * @prop {string} DialogHandlerCfg.pfdlgcid PrimeFaces dialog client ID.
     * @prop {string} DialogHandlerCfg.sourceComponentId ID of the dialog.
     * @prop {string} DialogHandlerCfg.sourceWidgetVar Widget variable of the dialog.
     * @prop {string} DialogHandlerCfg.url Source URL for the IFRAME element with the dialog.
     *
     * @interface {PrimeFaces.dialog.DialogHandlerCfgOptions} DialogHandlerCfgOptions Interface of the dialog
     * configuration object for a dialog of the dialog framework. Used by `PrimeFaces.dialog.DialogHandlerCfg`. This is
     * mainly just the `PrimeFaces.widget.DialogCfg`, but adds a few more properties.
     * @extends {PrimeFaces.widget.DialogCfg} DialogHandlerCfgOptions
     * @prop {number} DialogHandlerCfgOptions.contentHeight Height of the IFRAME in pixels.
     * @prop {number} DialogHandlerCfgOptions.contentWidth Width of the IFRAME in pixels.
     * @prop {string} DialogHandlerCfgOptions.headerElement ID of the header element of the dialog.
     * 
     * @interface {PrimeFaces.dialog.ExtendedConfirmDialogMessage} ExtendedConfirmDialogMessage An extended confirmation
     * message with an additional `source` attribute for specifying the source component or form.
     * @extends {PrimeFaces.widget.ConfirmDialog.ConfirmDialogMessage} ExtendedConfirmDialogMessage
     * @prop {string | HTMLElement | JQuery} source The source component (command button, AJAX callback etc) that
     * triggered the confirmation. When a string, it is interpreted as the client ID of the component. Otherwise, it
     * must be the main DOM element of the source component.
     */
    PrimeFaces.dialog = {};

    /**
     * The interface of the object with all methods for working with dialogs and the dialog framework.
     * @interface
     * @constant {PrimeFaces.dialog.DialogHandler} . The object with all methods for dialogs and the dialog framework.
     */
    PrimeFaces.dialog.DialogHandler = {

        /**
         * Opens the dialog as specified by the given configuration. When the dialog is dynamic, loads the content from
         * the server.
         * @param {PrimeFaces.dialog.DialogHandlerCfg} cfg Configuration of the dialog.
         */
        openDialog: function(cfg) {
            var rootWindow = this.findRootWindow(),
            dialogId = cfg.sourceComponentId + '_dlg';

            if(rootWindow.document.getElementById(dialogId)) {
                return;
            }

            // The widget that opens a dialog can be nested inside of a frame which might be nested again.
            // The dialog is put in the outermost frame to be able to fill the whole browser tab,
            // so we traverse upwards to find the root window and put the dialog DOM in there.
            // When a dialog is closed, we need to clean up the global variables and notify the source widget for the dialog return feature.
            // Accessing a component nested within frames requires recursive resolving of frames.
            // Every frame has it's own contentWindow and thus also it's own document object.
            // To be able to access a DOM element from an outer frame, one needs to first resolve the containing frame,
            // and then resolve the element from the contentWindow. With nested frames, nested frame resolving has to be done.
            // In order to do this, we traverse up the window frameElement until we reach the top window.
            // While traversing up, we construct a selector for finding the frameElement from within the parent window.
            // We build up the selectors backwards as we traverse up. Imagine the example
            //
            // --------------------------------------------------
            // | Frame 1                                        |
            // |           -------------------------------      |
            // |           | Frame 1_1                   |      |
            // |           |                             |      |
            // |           |  ------------               |      |
            // |           |  | Button 1 |               |      |
            // |           |  ------------               |      |
            // |           |                             |      |
            // |           -------------------------------      |
            // |------------------------------------------------|
            // | Frame 2                                        |
            // |                                                |
            // |                                                |
            // |                                                |
            // --------------------------------------------------
            //
            // Here "Button 1" is our source widget that opened the dialog.
            // The root window contains two frames "Frame 1" and "Frame 2".
            // The "Frame 1" contains another frame "Frame 1_1" within which the widget lives.
            // Since we have to install the dialog in the root window, we need to be able to get access
            // to the source widget when closing the dialog.
            // The only way to find the DOM node, is by traversing into "Frame 1" then into "Frame 1_1" and look it up there.
            // So from the root window we do e.g. `$(rootWindow.document).find("#frame1").contentWindow` to get into "Frame 1".
            // We do the same to get into "Frame 1_1" e.g. `$(frame1Window.document).find("#frame1_1").contentWindow`.
            // Finally, we can look up the source widget `$(frame1_1Window.document).find("#sourceWidgetId")`.

            var sourceFrames = function() {
                var w = window;
                var sourceFrames = [];
                // Traverse up frameElement i.e. while we are in frames
                while(w.frameElement) {
                    var parent = w.parent;
                    if (parent.PF === undefined) {
                        break;
                    }

                    // Since we traverse DOM elements upwards, we build the selector backwards i.e. from target to source.
                    // This is why we use `unshift` which is like an `addAtIndex(0, object)`.
                    // If an element has an id, we can use that to uniquely identify the DOM element and can jump to the next parent window.
                    // If we can't find an id, we collect class names and the tag name of an element.
                    // If that doesn't uniquely identify an element within it's parent, we also append the node index via the `:eq(index)` selector.
                    // We connect selectors for each DOM element with the `>` operator.
                    var e = w.frameElement;
                    var pieces = [];

                    // Traverse up tags from the frameElement to generate an identifying selector
                    for (; e && e.tagName !== undefined; e = e.parentNode) {
                        if (e.id && !/\s/.test(e.id)) {
                            // If we find a parent with an id, we can use that as basis and stop there
                            pieces.unshift(e.id);
                            pieces.unshift('#');
                            pieces.unshift(' > ');
                            break;
                        } else if (e.className) {
                            // Without an id, we try to use a combination of :eq, class names and tag name and hope a parent has an id
                            var classes = e.className.split(' ');
                            var classSelectorPieces = [];
                            for (var i in classes) {
                                if (classes.hasOwnProperty(i) && classes[i]) {
                                    classSelectorPieces.unshift(classes[i]);
                                    classSelectorPieces.unshift('.');
                                }
                            }
                            classSelectorPieces.unshift(e.tagName);

                            var classSelector = classSelectorPieces.join('');
                            var elems = $(e.parentNode).find(classSelector);
                            if (elems.length > 1) {
                                pieces.unshift(":eq(" + elems.index(e) + ")");
                            }
                            pieces.unshift(classSelector);
                        } else {
                            // Without classes, we try to work with :eq and the tag name
                            var elems = $(e.parentNode).find(e.tagName);
                            if (elems.length > 1) {
                                pieces.unshift(":eq(" + elems.index(e) + ")");
                            }
                            pieces.unshift(e.tagName);
                        }
                        pieces.unshift(' > ');
                    }

                    var s = pieces.slice(1).join('');

                    sourceFrames.unshift(s);
                    w = parent;
                };

                return sourceFrames;
            }();

            var dialogWidgetVar = cfg.options.widgetVar;
            if (!dialogWidgetVar) {
                dialogWidgetVar = cfg.sourceComponentId.replace(/:/g, '_') + '_dlgwidget';
            }

            var styleClass = cfg.options.styleClass||'',
            dialogDOM = $('<div id="' + dialogId + '" class="ui-dialog ui-widget ui-widget-content ui-corner-all ui-shadow ui-hidden-container ui-overlay-hidden ' + styleClass + '"' +
                    ' data-pfdlgcid="' + PrimeFaces.escapeHTML(cfg.pfdlgcid) + '" data-widget="' + dialogWidgetVar + '"></div>')
                    .append('<div class="ui-dialog-titlebar ui-widget-header ui-helper-clearfix ui-corner-top"><span id="' + dialogId + '_title" class="ui-dialog-title"></span></div>');

            var titlebar = dialogDOM.children('.ui-dialog-titlebar');
            if(cfg.options.closable !== false) {
                titlebar.append('<a class="ui-dialog-titlebar-icon ui-dialog-titlebar-close ui-corner-all" href="#" role="button"><span class="ui-icon ui-icon-closethick"></span></a>');
            }

            if(cfg.options.minimizable) {
                titlebar.append('<a class="ui-dialog-titlebar-icon ui-dialog-titlebar-minimize ui-corner-all" href="#" role="button"><span class="ui-icon ui-icon-minus"></span></a>');
            }

            if(cfg.options.maximizable) {
                titlebar.append('<a class="ui-dialog-titlebar-icon ui-dialog-titlebar-maximize ui-corner-all" href="#" role="button"><span class="ui-icon ui-icon-extlink"></span></a>');
            }

            dialogDOM.append('<div class="ui-dialog-content ui-widget-content ui-df-content" style="height: auto;">' +
                    '<iframe style="border:0 none" frameborder="0"></iframe>' +
                    '</div>');

            dialogDOM.appendTo(rootWindow.document.body);

            var dialogFrame = dialogDOM.find('iframe'),
            symbol = cfg.url.indexOf('?') === -1 ? '?' : '&',
            frameURL = cfg.url.indexOf('pfdlgcid') === -1 ? cfg.url + symbol + 'pfdlgcid=' + cfg.pfdlgcid: cfg.url,
            frameWidth = cfg.options.contentWidth||640;

            dialogFrame.width(frameWidth);

            if(cfg.options.iframeTitle) {
               dialogFrame.attr('title', cfg.options.iframeTitle);
            }

            dialogFrame.on('load', function() {
                var $frame = $(this),
                headerElement = $frame.contents().find('title'),
                isCustomHeader = false;

                if(cfg.options.headerElement) {
                    var customHeaderId = PrimeFaces.escapeClientId(cfg.options.headerElement),
                    customHeaderElement = dialogFrame.contents().find(customHeaderId);

                    if(customHeaderElement.length) {
                        headerElement = customHeaderElement;
                        isCustomHeader = true;
                    }
                }

                if(!$frame.data('initialized')) {
                    PrimeFaces.cw.call(rootWindow.PrimeFaces, 'DynamicDialog', dialogWidgetVar, {
                        id: dialogId,
                        position: cfg.options.position||'center',
                        sourceFrames: sourceFrames,
                        sourceComponentId: cfg.sourceComponentId,
                        sourceWidgetVar: cfg.sourceWidgetVar,
                        onHide: function() {
                            var $dialogWidget = this,
                            dialogFrame = this.content.children('iframe');

                            if(dialogFrame.get(0).contentWindow.PrimeFaces) {
                                this.destroyIntervalId = setInterval(function() {
                                    if(dialogFrame.get(0).contentWindow.PrimeFaces.ajax.Queue.isEmpty()) {
                                        clearInterval($dialogWidget.destroyIntervalId);
                                        dialogFrame.attr('src','about:blank');
                                        $dialogWidget.jq.remove();
                                    }
                                }, 10);
                            }
                            else {
                                dialogFrame.attr('src','about:blank');
                                $dialogWidget.jq.remove();
                            }

                            rootWindow.PrimeFaces.widgets[dialogWidgetVar] = undefined;
                        },
                        modal: cfg.options.modal,
                        blockScroll: cfg.options.blockScroll,
                        resizable: cfg.options.resizable,
                        hasIframe: true,
                        draggable: cfg.options.draggable,
                        width: cfg.options.width,
                        height: cfg.options.height,
                        minimizable: cfg.options.minimizable,
                        maximizable: cfg.options.maximizable,
                        headerElement: cfg.options.headerElement,
                        responsive: cfg.options.responsive,
                        closeOnEscape: cfg.options.closeOnEscape,
                        focus: cfg.options.focus
                    });
                }

                var title = rootWindow.PF(dialogWidgetVar).titlebar.children('span.ui-dialog-title');
                if(headerElement.length > 0) {
                    if(isCustomHeader) {
                        title.append(headerElement);
                        headerElement.show();
                    }
                    else {
                        title.text(headerElement.text());
                    }

                    dialogFrame.attr('title', title.text());
                }

                //adjust height
                var frameHeight = null;
                if(cfg.options.contentHeight) {
                    frameHeight = cfg.options.contentHeight;
                }
                else {
                    var frameBody = $frame.get(0).contentWindow.document.body;
                    var frameBodyStyle = window.getComputedStyle(frameBody);
                    frameHeight = frameBody.scrollHeight + parseFloat(frameBodyStyle.marginTop) + parseFloat(frameBodyStyle.marginBottom);
                }

                $frame.css('height', String(frameHeight));

                // fix #1290 - dialogs are not centered vertically
                dialogFrame.data('initialized', true);
                rootWindow.PF(dialogWidgetVar).show();
            })
            .attr('src', frameURL);
        },

        /**
         * Closes the dialog as specified by the given configuration.
         * @param {PrimeFaces.dialog.DialogHandlerCfg} cfg Configuration of the dialog.
         */
        closeDialog: function(cfg) {
            var rootWindow = this.findRootWindow(),
            dlgs = $(rootWindow.document.body).children('div.ui-dialog[data-pfdlgcid="' + $.escapeSelector(cfg.pfdlgcid) +'"]').not('[data-queuedforremoval]'),
            dlgsLength = dlgs.length,
            dlg = dlgs.eq(dlgsLength - 1),
            parentDlg = dlgsLength > 1 ? dlgs.eq(dlgsLength - 2) : null,
            dialogReturnBehavior = null,
            windowContext = null;

            var dlgWidget = rootWindow.PF(dlg.data('widget'));
            if(!dlgWidget) {
                // GitHub #2039 dialog may already be closed on slow internet
                PrimeFaces.error('Dialog widget was not found to close.');
                return;
            }

            var sourceWidgetVar = dlgWidget.cfg.sourceWidgetVar,
                sourceComponentId = dlgWidget.cfg.sourceComponentId;

            dlg.attr('data-queuedforremoval', true);

            if(parentDlg) {
                var parentDlgFrame = parentDlg.find('> .ui-dialog-content > iframe').get(0),
                windowContext = parentDlgFrame.contentWindow||parentDlgFrame;
                sourceWidget = windowContext.PF(sourceWidgetVar);
            }
            else {
                // We have to resolve the frames from the root window to the source widget to invoke the dialog return behavior
                // Each source frame element is a selector. We step into every nested frame until we are in the source widget frame.
                windowContext = rootWindow;
                var frames = dlgWidget.cfg.sourceFrames;
                for (var i = 0; i < frames.length; i++) {
                    windowContext = $(windowContext.document).find(frames[i]).get(0).contentWindow;
                }
            }

            if(sourceWidgetVar) {
                var sourceWidget = windowContext.PF(sourceWidgetVar);
                dialogReturnBehavior = sourceWidget.cfg.behaviors ? sourceWidget.cfg.behaviors['dialogReturn']: null;
            }
            else if(sourceComponentId) {
                var dialogReturnBehaviorStr = $(windowContext.document.getElementById(sourceComponentId)).data('dialogreturn');
                if(dialogReturnBehaviorStr) {
                    var dialogFunction = '(function(ext){this.' + dialogReturnBehaviorStr + '})';
                    if (PrimeFaces.csp.NONCE_VALUE) {
                        dialogReturnBehavior = PrimeFaces.csp.evalResult(dialogFunction);
                    }
                    else {
                        dialogReturnBehavior = windowContext.eval(dialogFunction);
                    }
                }
            }

            if(dialogReturnBehavior) {
                var ext = {
                        params: [
                            {name: sourceComponentId + '_pfdlgcid', value: cfg.pfdlgcid}
                        ]
                    };

                dialogReturnBehavior.call(windowContext, ext);
            }

            dlgWidget.hide();
        },

        /**
         * Displays a message in the messages dialog.
         * @param {PrimeFaces.widget.ConfirmDialog.ConfirmDialogMessage} msg Details of the essage to show.
         */
        showMessageInDialog: function(msg) {
            if(!this.messageDialog) {
                $('<div id="primefacesmessagedlg" class="ui-message-dialog ui-dialog ui-widget ui-widget-content ui-corner-all ui-shadow ui-hidden-container"></div>')
                            .append('<div class="ui-dialog-titlebar ui-widget-header ui-helper-clearfix ui-corner-top"><span class="ui-dialog-title"></span>' +
                            '<a class="ui-dialog-titlebar-icon ui-dialog-titlebar-close ui-corner-all" href="#" role="button"><span class="ui-icon ui-icon-closethick"></span></a></div>' +
                            '<div class="ui-dialog-content ui-widget-content" style="height: auto;"></div>')
                            .appendTo(document.body);

                PrimeFaces.cw('Dialog', 'primefacesmessagedialog', {
                    id: 'primefacesmessagedlg',
                    modal:true,
                    draggable: false,
                    resizable: false,
                    showEffect: 'fade',
                    hideEffect: 'fade'
                });
                this.messageDialog = PF('primefacesmessagedialog');
                this.messageDialog.titleContainer = this.messageDialog.titlebar.children('span.ui-dialog-title');
            }

            var escape = msg.escape !== false;
            var summaryHtml = msg.summary ? msg.summary.split(/\r\n|\n|\r/g).map(function(line) { return escape ? PrimeFaces.escapeHTML(line) : line; }).join("<br>") : "";
            this.messageDialog.titleContainer.html(summaryHtml);

            var detailHtml = msg.detail ? msg.detail.split(/\r\n|\n|\r/g).map(function(line) { return escape ? PrimeFaces.escapeHTML(line) : line; }).join("<br>") : "";
            this.messageDialog.content.html('').append('<span class="ui-dialog-message ui-messages-' + msg.severity.split(' ')[0].toLowerCase() + '-icon"></span>')
                .append('<span class="ui-dialog-message-content"></span');
            this.messageDialog.content.children('.ui-dialog-message-content').append(detailHtml);
            this.messageDialog.show();
        },

        /**
         * Asks the user to confirm an action. Shows a confirmation dialog with the given message. Requires a global
         * `<p:confirmDialog>` to be available on the current page.
         * @param {PrimeFaces.dialog.ExtendedConfirmDialogMessage} msg Message to show in the confirmation dialog.
         */
        confirm: function(msg) {
            if (PrimeFaces.confirmDialog) {
                PrimeFaces.confirmSource = (typeof(msg.source) === 'string') ? $(PrimeFaces.escapeClientId(msg.source)) : $(msg.source);
                PrimeFaces.confirmDialog.showMessage(msg);
            }
            else {
                PrimeFaces.warn('No global confirmation dialog available.');
            }
        },

        /**
         * Returns the current window instance. When inside an iframe, returns the window instance of the topmost
         * document.
         * @return {Window} The root window instance.
         */
        findRootWindow: function() {
            // Note that the determination of the sourceFrames is tightly coupled to the same traversing logic, so keep both in sync
            var w = window;
            while(w.frameElement) {
                var parent = w.parent;
                if (parent.PF === undefined) {
                	break;
                }
                w = parent;
            };

            return w;
        }
    };
}

/**
 * __PrimeFaces AccordionPanel Widget__
 * 
 * The AccordionPanel is a container component that displays content in a stacked format.
 * 
 * @prop {JQuery} headers The DOM elements for the header of each tab.
 * @prop {JQuery} panels The DOM elements for the content of each tab panel.
 * @prop {JQuery} stateHolder The DOM elements for the hidden input storing which panels are expanded and collapsed.
 * 
 * @interface {PrimeFaces.widget.AccordionPanelCfg} cfg The configuration for the
 * {@link  AccordionPanel| AccordionPanel 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 {number[]} cfg.active List of tabs that are currently active (open). Each item is a 0-based index of a tab.
 * @prop {boolean} cfg.cache `true` if activating a dynamic tab should not load the contents from server again and use
 * the cached contents; or `false` if the caching is disabled.
 * @prop {string} cfg.collapsedIcon The icon class name for the collapsed icon.
 * @prop {boolean} cfg.controlled `true` if a tab controller was specified for this widget; or `false` otherwise. A tab
 * controller is a server side listener that decides whether a tab change or tab close should be allowed.
 * @prop {boolean} cfg.dynamic `true` if the contents of each panel are loaded on-demand via AJAX; `false` otherwise.
 * @prop {string} cfg.expandedIcon The icon class name for the expanded icon.
 * @prop {boolean} cfg.multiple `true` if multiple tabs may be open at the same time; or `false` if opening one tab
 * closes all other tabs.
 * @prop {boolean} cfg.rtl `true` if the current text direction `rtl` (right-to-left); or `false` otherwise.
 */
PrimeFaces.widget.AccordionPanel = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.stateHolder = $(this.jqId + '_active');
        this.headers = this.jq.children('.ui-accordion-header');
        this.panels = this.jq.children('.ui-accordion-content');
        this.cfg.rtl = this.jq.hasClass('ui-accordion-rtl');
        this.cfg.expandedIcon = 'ui-icon-triangle-1-s';
        this.cfg.collapsedIcon = this.cfg.rtl ? 'ui-icon-triangle-1-w' : 'ui-icon-triangle-1-e';

        this.initActive();
        this.bindEvents();

        if(this.cfg.dynamic && this.cfg.cache) {
            this.markLoadedPanels();
        }
    },

    /**
     * Called when this accordion panel is initialized. Reads the selected panels from the saved state, see also
     * `saveState`.
     * @private
     */
    initActive: function() {
        var stateHolderVal = this.stateHolder.val();
        if (this.cfg.multiple) {
            this.cfg.active = [];

            if (stateHolderVal != null && stateHolderVal.length > 0) {
                var indexes = this.stateHolder.val().split(',');
                for(var i = 0; i < indexes.length; i++) {
                    this.cfg.active.push(parseInt(indexes[i]));
                }
            }
        }
        else if (stateHolderVal != null) {
            this.cfg.active = parseInt(this.stateHolder.val());
        }
    },

    /**
     * Binds all event listeners required by this accordion panel.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.headers.on("mouseover", function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')&&!element.hasClass('ui-state-disabled')) {
                element.addClass('ui-state-hover');
            }
        }).on("mouseout", function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')&&!element.hasClass('ui-state-disabled')) {
                element.removeClass('ui-state-hover');
            }
        }).on("click", function(e) {
            var element = $(this);
            if(!element.hasClass('ui-state-disabled')) {
                var tabIndex = $this.headers.index(element);

                if(element.hasClass('ui-state-active')) {
                    $this.unselect(tabIndex);
                }
                else {
                    $this.select(tabIndex);
                    $(this).trigger('focus.accordion');
                }
            }

            e.preventDefault();
        });

        this.bindKeyEvents();
    },

    /**
     * Sets up all event listeners for keyboard interactions.
     * @private
     */
    bindKeyEvents: function() {
        this.headers.on('focus.accordion', function(){
            $(this).addClass('ui-tabs-outline');
        })
        .on('blur.accordion', function(){
            $(this).removeClass('ui-tabs-outline');
        })
        .on('keydown.accordion', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                $(this).trigger('click');
                e.preventDefault();
            }
        });
    },

    /**
     * Marks the currently active panels as loaded; their content does not need to be retrieved from the server anymore.
     * @private
     */
    markLoadedPanels: function() {
        if(this.cfg.multiple) {
            for(var i = 0; i < this.cfg.active.length; i++) {
                if(this.cfg.active[i] >= 0)
                    this.markAsLoaded(this.panels.eq(this.cfg.active[i]));
            }
        } else {
            if(this.cfg.active >= 0)
                this.markAsLoaded(this.panels.eq(this.cfg.active));
        }
    },

    /**
     * Activates (opens) the tab with given index. This may fail by returning `false`, such
     * as when a callback is registered that prevent the tab from being opened.
     * @param {number} index 0-based index of the tab to open. Must not be out of range.
     * @return {boolean} `true` when the given panel is now active, `false` otherwise. 
     */
    select: function(index) {
        var panel = this.panels.eq(index),
            header = panel.prev();

        // don't select already selected panel
        if (header.hasClass('ui-state-active')) {
            return;
        }

        //Call user onTabChange callback
        if(this.cfg.onTabChange) {
            var result = this.cfg.onTabChange.call(this, panel);
            if(result === false)
                return false;
        }

        var shouldLoad = this.cfg.dynamic && !this.isLoaded(panel);

        //update state
        if(this.cfg.multiple)
            this.addToSelection(index);
        else
            this.cfg.active = index;

        this.saveState();

        if(shouldLoad) {
            this.loadDynamicTab(panel);
        }
        else {
            if(this.cfg.controlled) {
                this.fireTabChangeEvent(panel);
            }
            else {
                this.show(panel);

                this.fireTabChangeEvent(panel);
            }

        }

        return true;
    },

    /**
     * Activates (opens) all the tabs if multiple mode is enabled and the first tab in single mode.
     */
    selectAll: function() {
        var $this = this;
        this.panels.each(function(index) {
            $this.select(index);
            if (!$this.cfg.multiple) {
                return false; // breaks
            }
        });
    },

    /**
     * Deactivates (closes) the tab with given index.
     * @param {number} index 0-based index of the tab to close. Must not be out of range.
     */
    unselect: function(index) {
        var panel = this.panels.eq(index),
            header = panel.prev();

        // don't unselect already unselected panel
        if (!header.hasClass('ui-state-active')) {
            return;
        }
        
        if(this.cfg.controlled) {
            this.fireTabCloseEvent(index);
        }
        else {
            this.hide(index);

            this.fireTabCloseEvent(index);
        }
    },

    /**
     * Deactivates (closes) all the tabs.
     */
    unselectAll: function() {
        var $this = this;
        this.panels.each(function(index) {
            $this.unselect(index);
        });
    },

    /**
     * Hides other panels and makes the given panel visible, such as by adding or removing the appropriate CSS classes.
     * @private
     * @param {JQuery} panel A tab panel to show.
     */
    show: function(panel) {
        var $this = this;

        //deactivate current
        if(!this.cfg.multiple) {
            var oldHeader = this.headers.filter('.ui-state-active');
            oldHeader.children('.ui-icon').removeClass(this.cfg.expandedIcon).addClass(this.cfg.collapsedIcon);
            oldHeader.attr('aria-selected', false);
            oldHeader.attr('aria-expanded', false).removeClass('ui-state-active ui-corner-top').addClass('ui-corner-all')
                .next().attr('aria-hidden', true).slideUp(function(){
                    if($this.cfg.onTabClose)
                        $this.cfg.onTabClose.call($this, panel);
                });
        }

        //activate selected
        var newHeader = panel.prev();
        newHeader.attr('aria-selected', true);
        newHeader.attr('aria-expanded', true).addClass('ui-state-active ui-corner-top').removeClass('ui-state-hover ui-corner-all')
                .children('.ui-icon').removeClass(this.cfg.collapsedIcon).addClass(this.cfg.expandedIcon);

        panel.attr('aria-hidden', false).slideDown('normal', function() {
            $this.postTabShow(panel);
        });
    },

    /**
     * Hides one of the panels of this accordion.
     * @private
     * @param {number} index 0-based index of the panel to hide.
     */
    hide: function(index) {
        var $this = this,
        panel = this.panels.eq(index),
        header = panel.prev();

        header.attr('aria-selected', false);
        header.attr('aria-expanded', false).children('.ui-icon').removeClass(this.cfg.expandedIcon).addClass(this.cfg.collapsedIcon);
        header.removeClass('ui-state-active ui-corner-top').addClass('ui-corner-all');
        panel.attr('aria-hidden', true).slideUp(function(){
            if($this.cfg.onTabClose)
                $this.cfg.onTabClose.call($this, panel);
        });

        this.removeFromSelection(index);
        this.saveState();
    },

    /**
     * The content of a tab panel may be loaded dynamically on demand via AJAX. This method loads the content of the
     * given tab. Make sure to check first that this widget has got a dynamic tab panel (see
     * {@link AccordionPanelCfg.dynamic}) and that the given tab panel is not loaded already (see {@link isLoaded}).
     * @param {JQuery} panel A tab panel to load.
     */
    loadDynamicTab: function(panel) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true},
                {name: this.id + '_newTab', value: panel.attr('id')},
                {name: this.id + '_tabindex', value: parseInt(panel.index() / 2)}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            panel.html(content);

                            if(this.cfg.cache) {
                                this.markAsLoaded(panel);
                            }
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.show(panel);
            }
        };

        if(this.hasBehavior('tabChange')) {
            this.callBehavior('tabChange', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Handles the event listeners and behaviors when switching to a different tab.
     * @private
     * @param {JQueryStatic} panel The tab which is now active.
     */
    fireTabChangeEvent : function(panel) {
        if(this.hasBehavior('tabChange')) {
            var ext = {
                params: [
                    {name: this.id + '_newTab', value: panel.attr('id')},
                    {name: this.id + '_tabindex', value: parseInt(panel.index() / 2)}
                ]
            };

            if(this.cfg.controlled) {
                var $this = this;
                ext.oncomplete = function(xhr, status, args, data) {
                    if(args.access && !args.validationFailed) {
                        $this.show(panel);
                    }
                };
            }

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

    /**
     * Handles the event listeners and behaviors when a tab was closed.
     * @private
     * @param {number} index 0-based index of the closed tab.
     */
    fireTabCloseEvent : function(index) {
        if(this.hasBehavior('tabClose')) {
            var panel = this.panels.eq(index),
            ext = {
                params: [
                    {name: this.id + '_tabId', value: panel.attr('id')},
                    {name: this.id + '_tabindex', value: parseInt(index)}
                ]
            };

            if(this.cfg.controlled) {
                var $this = this;
                ext.oncomplete = function(xhr, status, args, data) {
                    if(args.access && !args.validationFailed) {
                        $this.hide(index);
                    }
                };
            }

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

    /**
     * When loading tab content dynamically, marks the content as loaded.
     * @private
     * @param {JQuery} panel A panel of this accordion that was loaded.
     */
    markAsLoaded: function(panel) {
        panel.data('loaded', true);
    },

    /**
     * The content of a tab panel may be loaded dynamically on demand via AJAX. This method checks whether the content
     * of a tab panel is currently loaded.
     * @param {JQuery} panel A tab panel to check.
     * @return {boolean} `true` if the content of the tab panel is loaded, `false` otherwise.
     */
    isLoaded: function(panel) {
        return panel.data('loaded') == true;
    },

    /**
     * Adds the given panel node to the list of currently selected nodes.
     * @private
     * @param {string} nodeId ID of a panel node.
     */
    addToSelection: function(nodeId) {
        this.cfg.active.push(nodeId);
    },

    /**
     * Removes the given panel node from the list of currently selected nodes.
     * @private
     * @param {string} nodeId ID of a panel node.
     */
    removeFromSelection: function(nodeId) {
        this.cfg.active = $.grep(this.cfg.active, function(r) {
            return r != nodeId;
        });
    },

    /**
     * Saves the current state of this widget, used for example to preserve the state during AJAX updates.
     * @private
     */
    saveState: function() {
        if(this.cfg.multiple)
            this.stateHolder.val(this.cfg.active.join(','));
        else
            this.stateHolder.val(this.cfg.active);
    },

    /**
     * Handles event listeners and behaviors when switching to a different tab.
     * @private
     * @param {JQuery} newPanel The new tab the is shown.
     */
    postTabShow: function(newPanel) {
        //Call user onTabShow callback
        if(this.cfg.onTabShow) {
            this.cfg.onTabShow.call(this, newPanel);
        }

        PrimeFaces.invokeDeferredRenders(this.id);
    }

});

/**
 * __PrimeFaces AutoComplete Widget__
 * 
 * AutoComplete provides live suggestions while the user is entering text
 * 
 * @typedef {"blank" | "current"} PrimeFaces.widget.AutoComplete.DropdownMode Specifies the behavior of the dropdown
 * button.
 * - `blank`: Sends an empty string.
 * - `current`: Send the input value.
 * 
 * @typedef {"keyup" | "enter"} PrimeFaces.widget.AutoComplete.QueryEvent  Event to initiate the autocomplete search.
 * - `enter`: Starts the search for suggestion items when the enter key is pressed.
 * - `keyup`: Starts the search for suggestion items as soon as a key is released.
 * 
 * @typedef {"server" | "client" | "hybrid"} PrimeFaces.widget.AutoComplete.QueryMode Specifies whether filter requests
 * are evaluated by the client's browser or whether they are sent to the server.
 * 
 * @typedef PrimeFaces.widget.AutoComplete.OnChangeCallback Client side callback to invoke when value changes.
 * @param {JQuery} PrimeFaces.widget.AutoComplete.OnChangeCallback.input (Input) element on which the change occurred.
 * 
 * @prop {boolean} active Whether the autocomplete is active.
 * @prop {Record<string, string>} [cache] The cache for the results of an autocomplete search.
 * @prop {number} [cacheTimeout] The set-interval timer ID for the cache timeout. 
 * @prop {JQuery} dropdown The DOM element for the container with the dropdown suggestions.
 * @prop {JQuery} input The DOM element for the input element.
 * @prop {boolean} isDynamicLoaded If dynamic loading is enabled, whether the content was loaded already.
 * @prop {boolean} isTabPressed Whether the tab key is currently pressed.
 * @prop {JQuery} hinput The DOM element for the hidden input with the selected value.
 * @prop {JQuery} [items] The DOM elements for the suggestion items.
 * @prop {JQuery} itemtip The DOM element for the tooltip of a suggestion item.
 * @prop {boolean} [itemClick] Whether an item was clicked.
 * @prop {JQuery} [itemContainer] The DOM element for the container with the suggestion items.
 * @prop {boolean} [itemSelectedWithEnter] Whether an item was selected via the enter key.
 * @prop {JQuery} [multiItemContainer] The DOM element for the container with multiple selection items.
 * @prop {JQuery} panel The DOM element for the overlay panel with the suggestion items. 
 * @prop {string} panelId The client ID of the overlay panel with the suggestion items.
 * @prop {JQuery} status The DOM element for the autocomplete status ARIA element.
 * @prop {boolean} suppressInput Whether key input events should be ignored currently.
 * @prop {boolean} touchToDropdownButton Whether a touch is made on the dropdown button.
 * @prop {string} wrapperStartTag The starting HTML with the wrapper element of the suggestions box.  
 * @prop {string} wrapperEndTag The finishing HTML with the wrapper element of the suggestions box.
 * 
 * @interface {PrimeFaces.widget.AutoCompleteCfg} cfg The configuration for the {@link  AutoComplete| AutoComplete 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.active Whether autocompletion search is initially active.
 * @prop {string} cfg.appendTo ID of the container to which the suggestion box is appended.
 * @prop {string} cfg.atPos Defines which position on the target element to align the positioned element against.
 * @prop {boolean} cfg.autoHighlight Highlights the first suggested item automatically.
 * @prop {boolean} cfg.autoSelection Defines if auto selection of items that are equal to the typed input is enabled. If
 * `true`, an item that is equal to the typed input is selected.
 * @prop {boolean} cfg.cache When enabled autocomplete caches the searched result list.
 * @prop {number} cfg.cacheTimeout Timeout in milliseconds value for cached results.
 * @prop {number} cfg.delay The delay in milliseconds before an autocomplete search is triggered.
 * @prop {PrimeFaces.widget.AutoComplete.DropdownMode} cfg.dropdownMode Specifies the behavior of the dropdown button.
 * @prop {boolean} cfg.dynamic Defines if dynamic loading is enabled for the element's panel. If the value is `true`,
 * the overlay is not rendered on page load to improve performance.
 * @prop {string} cfg.emptyMessage Text to display when there is no data to display.
 * @prop {boolean} cfg.escape Whether the text of the suggestion items is escaped for HTML.
 * @prop {boolean} cfg.forceSelection Whether one of the available suggestion items is forced to be preselected.
 * @prop {boolean} cfg.grouping Whether suggestion items are grouped.
 * @prop {boolean} cfg.itemtip Whether a tooltip is shown for the suggestion items.
 * @prop {string} cfg.itemtipAtPosition Position of item corner relative to item tip.
 * @prop {string} cfg.itemtipMyPosition Position of itemtip corner relative to item.
 * @prop {number} cfg.minLength Minimum length before an autocomplete search is triggered.
 * @prop {boolean} cfg.multiple When `true`, enables multiple selection.
 * @prop {string} cfg.myPos Defines which position on the element being positioned to align with the target element.
 * @prop {PrimeFaces.widget.AutoComplete.OnChangeCallback} cfg.onChange Client side callback to invoke when value
 * changes.
 * @prop {PrimeFaces.widget.AutoComplete.QueryEvent} cfg.queryEvent Event to initiate the the autocomplete search.
 * @prop {PrimeFaces.widget.AutoComplete.QueryMode} cfg.queryMode Specifies query mode, whether autocomplete contacts
 * the server.
 * @prop {string} cfg.resultsMessage Hint text for screen readers to provide information about the search results.
 * @prop {number} cfg.selectLimit Limits the number of simultaneously selected items. Default is unlimited.
 * @prop {number} cfg.scrollHeight Height of the container with the suggestion items.
 * @prop {boolean} cfg.unique Ensures uniqueness of the selected items.
 * @prop {string} cfg.completeEndpoint REST endpoint for fetching autocomplete suggestions. Takes precedence over the
 * bean command specified via `completeMethod` on the component. 
 * @prop {string} cfg.moreText The text shown in the panel when the number of suggestions is greater than `maxResults`.
 */
PrimeFaces.widget.AutoComplete = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.panelId = this.jqId + '_panel';
        this.input = $(this.jqId + '_input');
        this.hinput = $(this.jqId + '_hinput');
        this.panel = this.jq.children(this.panelId);
        this.dropdown = this.jq.children('.ui-button');
        this.active = true;
        this.cfg.pojo = this.hinput.length == 1;
        this.cfg.minLength = this.cfg.minLength != undefined ? this.cfg.minLength : 1;
        this.cfg.cache = this.cfg.cache||false;
        this.cfg.resultsMessage = this.cfg.resultsMessage||' results are available, use up and down arrow keys to navigate';
        this.cfg.ariaEmptyMessage = this.cfg.emptyMessage||'No search results are available.';
        this.cfg.dropdownMode = this.cfg.dropdownMode||'blank';
        this.cfg.autoHighlight = (this.cfg.autoHighlight === undefined) ? true : this.cfg.autoHighlight;
        this.cfg.appendTo = PrimeFaces.utils.resolveAppendTo(this, this.panel);
        this.cfg.myPos = this.cfg.myPos||'left top';
        this.cfg.atPos = this.cfg.atPos||'left bottom';
        this.cfg.active = (this.cfg.active === false) ? false : true;
        this.cfg.dynamic = this.cfg.dynamic === true ? true : false;
        this.cfg.autoSelection = this.cfg.autoSelection === false ? false : true;
        this.cfg.escape = this.cfg.escape === false ? false : true;
        this.suppressInput = true;
        this.touchToDropdownButton = false;
        this.isTabPressed = false;
        this.isDynamicLoaded = false;

        if(this.cfg.cache) {
            this.initCache();
        }
        
        if (this.cfg.queryMode !== 'server') {
            this.fetchItems();
        }

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        this.hinput.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        
        this.placeholder = this.input.attr('placeholder');

        if(this.cfg.multiple) {
            this.setupMultipleMode();

            this.multiItemContainer.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

            if(this.cfg.selectLimit >= 0 && this.multiItemContainer.children('li.ui-autocomplete-token').length === this.cfg.selectLimit) {
                this.input.hide();
                this.disableDropdown();
            }
        }
        else {
            //visuals
            PrimeFaces.skinInput(this.input);

            this.input.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);
            this.dropdown.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);
        }

        //core events
        this.bindStaticEvents();

        //client Behaviors
        if(this.cfg.behaviors) {
            PrimeFaces.attachBehaviors(this.input, this.cfg.behaviors);
        }

        //force selection
        if(this.cfg.forceSelection) {
            this.setupForceSelection();
        }

        //Panel management
        if(this.panel.length) {
            this.appendPanel();
            this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
        }

        //itemtip
        if(this.cfg.itemtip) {
            this.itemtip = $('<div id="' + this.id + '_itemtip" class="ui-autocomplete-itemtip ui-state-highlight ui-widget ui-corner-all ui-shadow"></div>').appendTo(document.body);
            this.cfg.itemtipMyPosition = this.cfg.itemtipMyPosition||'left top';
            this.cfg.itemtipAtPosition = this.cfg.itemtipAtPosition||'right bottom';
            this.cfg.checkForScrollbar = (this.cfg.itemtipAtPosition.indexOf('right') !== -1);
        }

        //aria
        this.input.attr('aria-autocomplete', 'list');
        this.jq.attr('role', 'application');
        this.jq.append('<span role="status" aria-live="polite" class="ui-autocomplete-status ui-helper-hidden-accessible"></span>');
        this.status = this.jq.children('.ui-autocomplete-status');
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);
    },

    /**
     * Appends the overlay panel to the DOM.
     * @private
     */
    appendPanel: function() {
        PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');
    },

    /**
     * Initializes the cache that stores the retrieved suggestions for a search term.
     * @private
     */
    initCache: function() {
        this.cache = {};
        var $this=this;

        this.cacheTimeout = setInterval(function(){
            $this.clearCache();
        }, this.cfg.cacheTimeout);
    },

    /**
     * Clears the cache with the results of an autocomplete search.
     * @private
     */
    clearCache: function() {
        this.cache = {};
    },

    /**
     * Binds events for multiple selection mode
     * @private
     */
    setupMultipleMode: function() {
        var $this = this;
        this.multiItemContainer = this.jq.children('ul');
        this.inputContainer = this.multiItemContainer.children('.ui-autocomplete-input-token');

        this.multiItemContainer.on("mouseenter", function() {
                $(this).addClass('ui-state-hover');
        }).on("mouseleave", function() {
                $(this).removeClass('ui-state-hover');
        }).on("click", function() {
            $this.input.trigger('focus');
        });

        //delegate events to container
        this.input.on("focus", function() {
            $this.multiItemContainer.addClass('ui-state-focus');
        }).on("blur", function(e) {
            $this.multiItemContainer.removeClass('ui-state-focus');
        });

        var closeSelector = '> li.ui-autocomplete-token > .ui-autocomplete-token-icon';
        this.multiItemContainer.off('click', closeSelector).on('click', closeSelector, null, function(event) {
            if($this.multiItemContainer.children('li.ui-autocomplete-token').length === $this.cfg.selectLimit) {
                $this.input.css('display', 'inline');
                $this.enableDropdown();
            }
            $this.removeItem($(this).parent());
        });
    },

    /**
     * Sets up all global event listeners for the overlay.
     * @private
     */
    bindStaticEvents: function() {
        var $this = this;

        this.bindKeyEvents();

        this.bindDropdownEvents();

        if(PrimeFaces.env.browser.mobile) {
            this.dropdown.on('touchstart', function() {
                $this.touchToDropdownButton = true;
            });
        }
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.panel,
            function() { return $this.itemtip; },
            function(e, eventTarget) {
                if (!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    $this.hide();
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.panel, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Sets up all event listeners for the dropdown menu.
     * @private
     */
    bindDropdownEvents: function() {
        var $this = this;

        PrimeFaces.skinButton(this.dropdown);

        this.dropdown.on("mouseup", function() {
            if($this.active) {
                $this.searchWithDropdown();
                $this.input.trigger('focus');
            }
        }).on("keyup", function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                $this.searchWithDropdown();
                $this.input.trigger('focus');
                e.preventDefault();
                e.stopPropagation();
            }
        });
    },

    /**
     * Disables the dropdown menu.
     * @private
     */
    disableDropdown: function() {
        if(this.dropdown.length) {
            this.dropdown.off().prop('disabled', true).addClass('ui-state-disabled');
        }
    },

    /**
     * Enables the dropdown menu.
     * @private
     */
    enableDropdown: function() {
        if(this.dropdown.length && this.dropdown.prop('disabled')) {
            this.bindDropdownEvents();
            this.dropdown.prop('disabled', false).removeClass('ui-state-disabled');
        }
    },

    /**
     * Sets up all keyboard related event listeners.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        // GitHub #6711 use DOM if non-CSP and JQ event if CSP
        var originalOnchange = this.input.prop('onchange');
        if (!originalOnchange && this.input[0]) {
            var events = $._data(this.input[0], "events");
            if(events.change) {
                originalOnchange = events.change[0].handler;
            }
        }
        this.cfg.onChange = originalOnchange;
        if (originalOnchange) {
            this.input.prop('onchange', null).off('change');
        }

        if(this.cfg.queryEvent !== 'enter') {
            this.input.on('input propertychange', function(e) {
                $this.processKeyEvent(e);
            });
        }

        this.input.on('keyup.autoComplete', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(PrimeFaces.env.isIE(9) && (key === keyCode.BACKSPACE || key === keyCode.DELETE)) {
                $this.processKeyEvent(e);
            }

            if($this.cfg.queryEvent === 'enter' && (key === keyCode.ENTER)) {
                if($this.itemSelectedWithEnter)
                    $this.itemSelectedWithEnter = false;
                else
                    $this.search($this.input.val());
            }

            if($this.panel.is(':visible')) {
                if(key === keyCode.ESCAPE) {
                    $this.hide();
                }
                else if(key === keyCode.UP || key === keyCode.DOWN) {
                    var highlightedItem = $this.items.filter('.ui-state-highlight');
                    if(highlightedItem.length) {
                        $this.displayAriaStatus(highlightedItem.data('item-label'));
                    }
                }
            }

            $this.checkMatchedItem = true;
            $this.isTabPressed = false;

        }).on('keydown.autoComplete', function(e) {
            var keyCode = $.ui.keyCode;

            $this.suppressInput = false;
            if($this.panel.is(':visible')) {
                var highlightedItem = $this.items.filter('.ui-state-highlight');

                switch(e.which) {
                    case keyCode.UP:
                        var prev = highlightedItem.length == 0 ? $this.items.eq(0) : highlightedItem.prevAll('.ui-autocomplete-item:first');

                        if(prev.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            prev.addClass('ui-state-highlight');

                            if($this.cfg.scrollHeight) {
                                PrimeFaces.scrollInView($this.panel, prev);
                            }

                            if($this.cfg.itemtip) {
                                $this.showItemtip(prev);
                            }
                        }

                        e.preventDefault();
                        break;

                    case keyCode.DOWN:
                        var next = highlightedItem.length == 0 ? $this.items.eq(0) : highlightedItem.nextAll('.ui-autocomplete-item:first');

                        if(next.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            next.addClass('ui-state-highlight');

                            if($this.cfg.scrollHeight) {
                                PrimeFaces.scrollInView($this.panel, next);
                            }

                            if($this.cfg.itemtip) {
                                $this.showItemtip(next);
                            }
                        }

                        e.preventDefault();
                        break;

                    case keyCode.ENTER:
                        if ($this.timeout) {
                            $this.deleteTimeout();
                        }

                        if (highlightedItem.length > 0) {
                            $this.preventInputChangeEvent = true;
                            highlightedItem.trigger("click");
                            $this.itemSelectedWithEnter = true;
                        }

                        e.preventDefault();
                        e.stopPropagation();

                        break;

                    case 18: //keyCode.ALT:
                    case 224:
                        break;

                    case keyCode.TAB:
                        if(highlightedItem.length && $this.cfg.autoSelection) {
                            highlightedItem.trigger('click');
                        } else {
                            $this.hide();
                            if ($this.timeout) {
                                $this.deleteTimeout();
                            }
                        }
                        $this.isTabPressed = true;
                        break;
                }
            }
            else {
                switch(e.which) {
                    case keyCode.TAB:
                        if ($this.timeout) {
                            $this.deleteTimeout();
                        }
                        $this.isTabPressed = true;
                    break;

                    case keyCode.ENTER:
                        var itemValue = $(this).val();
                        if($this.cfg.queryEvent === 'enter' || ($this.timeout > 0) || $this.querying) {
                            e.preventDefault();
                        }

                        if($this.cfg.queryEvent !== 'enter') {
                            $this.isValid(itemValue, true);
                        }

                        if($this.cfg.multiple && itemValue) {
                            $this.addItem(itemValue);
                            e.preventDefault();
                            e.stopPropagation();
                        }
                    break;

                    case keyCode.BACKSPACE:
                        if ($this.cfg.multiple && !$this.input.val().length) {
                            
                            if (e.metaKey||e.ctrlKey||e.shiftKey) {
                                $this.removeAllItems();
                            } else {
                                $this.removeItem($(this).parent().prev());
                            }

                            e.preventDefault();
                        }
                    break;
                };
            }

        }).on('paste.autoComplete', function() {
            $this.suppressInput = false;
            $this.checkMatchedItem = true;
	    }).on('change.autoComplete', function(e) {
            if ($this.cfg.onChange && !$this.preventInputChangeEvent) {
                $this.cfg.onChange.call(this);
            }
            
            $this.preventInputChangeEvent = false;
        });
    },

    /**
     * Sets up all event listeners for mouse and click events.
     * @private
     */
    bindDynamicEvents: function() {
        var $this = this;

        //visuals and click handler for items
        this.items.off('click.autocomplete mousedown.autocomplete mouseover.autocomplete')
        .on('mouseover.autocomplete', function() {
            var item = $(this);

            if(!item.hasClass('ui-state-highlight')) {
                $this.items.filter('.ui-state-highlight').removeClass('ui-state-highlight');
                item.addClass('ui-state-highlight');

                if($this.cfg.itemtip) {
                    $this.showItemtip(item);
                }
            }
        })
        .on('click.autocomplete', function(event) {
            var item = $(this),
            isMoreText = item.hasClass('ui-autocomplete-moretext');

            if(isMoreText) {
                $this.input.trigger('focus');
                $this.invokeMoreTextBehavior();
            }
            else {
                $this.addItem(item);
            }

            $this.hide();
        })
        .on('mousedown.autocomplete', function() {
            $this.preventInputChangeEvent = true;
            $this.checkMatchedItem = false;
        });

        this.panel.on('click.emptyMessage', function() {
            if (!this.children) {
                return;
            }
            var item = $(this.children[0]),
            isEmptyMessage = item.hasClass('ui-autocomplete-emptyMessage');

            if(isEmptyMessage) {
                $this.invokeEmptyMessageBehavior();
            }
        });

        if(PrimeFaces.env.browser.mobile) {
            this.items.on('touchstart.autocomplete', function() {
                if(!$this.touchToDropdownButton) {
                    $this.itemClick = true;
                }
            });
        }
    },

    /**
     * Callback for when a key event occurred.
     * @private
     * @param {JQuery.TriggeredEvent} e Key event that occurred.
     */
    processKeyEvent: function(e) {
        var $this = this;

        if($this.suppressInput) {
            e.preventDefault();
            return;
        }

        // for touch event on mobile
        if(PrimeFaces.env.browser.mobile) {
            $this.touchToDropdownButton = false;
            if($this.itemClick) {
                $this.itemClick = false;
                return;
            }
        }

        var value = $this.input.val();

        if($this.cfg.pojo && !$this.cfg.multiple) {
            $this.hinput.val(value);
        }

        if(!value.length) {
            $this.hide();
            $this.deleteTimeout();
        }

        if(value.length >= $this.cfg.minLength) {
            if($this.timeout) {
                $this.deleteTimeout();
            }

            var delay = $this.cfg.delay;
            if(delay && delay > 0) {
                $this.timeout = setTimeout(function() {
                    $this.timeout = null;
                    $this.search(value);
                }, delay);
            }
            else {
                 $this.search(value);
            } 
        }
        else if(value.length === 0) {
            if($this.timeout) {
                $this.deleteTimeout();
            }
            $this.fireClearEvent();
        }
    },

    /**
     * Shows the tooltip for the given suggestion item.
     * @private
     * @param {JQuery} item Item with a tooltip.
     */
    showItemtip: function(item) {
        if(item.hasClass('ui-autocomplete-moretext')) {
            this.itemtip.hide();
        }
        else {
            var content;
            if(item.is('li')) {
                content = item.next('.ui-autocomplete-itemtip-content');
            } else {
                if(item.children('td:last').hasClass('ui-autocomplete-itemtip-content')) {
                    content = item.children('td:last');
                } else {
                    this.itemtip.hide();
                    return;
                }
            }

            this.itemtip.html(content.html())
                        .css({
                            'left':'',
                            'top':'',
                            'z-index': PrimeFaces.nextZindex(),
                            'width': content.outerWidth() + 'px'
                        })
                        .position({
                            my: this.cfg.itemtipMyPosition
                            ,at: this.cfg.itemtipAtPosition
                            ,of: item
                        });

            //scrollbar offset
            if(this.cfg.checkForScrollbar) {
                if(this.panel.innerHeight() < this.panel.children('.ui-autocomplete-items').outerHeight(true)) {
                    var panelOffset = this.panel.offset();
                    this.itemtip.css('left', (panelOffset.left + this.panel.outerWidth()) + 'px');
                }
            }

            this.itemtip.show();
        }
    },

    /**
     * Performs the search for the available suggestion items.
     * @private
     * @param {string} query Keyword for the search. 
     */
    showSuggestions: function(query) {
        this.items = this.panel.find('.ui-autocomplete-item');
        this.items.attr('role', 'option');

        if(this.cfg.grouping) {
            this.groupItems();
        }

        this.bindDynamicEvents();

        var $this=this,
        hidden = this.panel.is(':hidden');

        if(hidden) {
            this.show();
        }
        else {
            this.alignPanel();
        }

        if(this.items.length > 0) {
            var firstItem = this.items.eq(0);

            //highlight first item
            if(this.cfg.autoHighlight && firstItem.length) {
                firstItem.addClass('ui-state-highlight');
            }

            //highlight query string
            if(this.panel.children().is('ul') && query.length > 0) {
                this.items.filter(':not(.ui-autocomplete-moretext)').each(function() {
                    var item = $(this);
                    var text = $this.cfg.escape ? item.html() : item.text();
                    var re = new RegExp(PrimeFaces.escapeRegExp(query), 'gi'),
                    highlighedText = text.replace(re, '<span class="ui-autocomplete-query">$&</span>');

                    item.html(highlighedText);
                });
            }

            if(this.cfg.forceSelection) {
                this.currentItems = [];
                this.items.each(function(i, item) {
                    $this.currentItems.push($(item).attr('data-item-label'));
                });
            }

            //show itemtip if defined
            if(this.cfg.autoHighlight && this.cfg.itemtip && firstItem.length === 1) {
                this.showItemtip(firstItem);
            }

            this.displayAriaStatus(this.items.length + this.cfg.resultsMessage);
        }
        else {
            if(this.cfg.emptyMessage) {
                var emptyText = '<div class="ui-autocomplete-emptyMessage ui-widget">' + PrimeFaces.escapeHTML(this.cfg.emptyMessage) + '</div>';
                this.panel.html(emptyText);
            }
            else {
                this.panel.hide();
            }

            this.displayAriaStatus(this.cfg.ariaEmptyMessage);
        }
    },

    /**
     * Performs a search the same ways as if the user had opened the dropdown menu. Depending on the configured
     * `dropdownMode`, performs the search either with an empty string or with the current value.
     */
    searchWithDropdown: function() {
        this.isSearchWithDropdown = true;

        if(this.cfg.dropdownMode === 'current')
            this.search(this.input.val());
        else
            this.search('');
    },

    /**
     * Initiates a search with given value, that is, look for matching options and present the options that were found
     * to the user.
     * @param {string} query Keyword for the search. 
     */
    search: function(query) {
        //allow empty string but not undefined or null
        if (!this.cfg.active || query === undefined || query === null) {
            return;
        }

        if (this.cfg.cache && !(this.cfg.dynamic && !this.isDynamicLoaded)) {
            if (this.cache[query]) {
                this.panel.html(this.cache[query]);
                this.showSuggestions(query);
                return;
            }
            else if (this.cfg.queryMode === 'client') {
                if (this.isSearchWithDropdown) {
                    var suggestions = this.wrapperStartTag,
                        re = new RegExp(this.wrapperStartTag + '|' + this.wrapperEndTag, 'g');
                    Object.entries(this.cache).map(function(item) {
                        suggestions += item[1].replace(re, '');
                    });
                    suggestions += this.wrapperEndTag;
                    
                    this.panel.html(suggestions);
                    
                    this.isSearchWithDropdown = false;
                }
                else {
                    this.panel.empty();
                }
                
                this.showSuggestions(query);
                return;
            }
        }

        if (!this.active) {
            return;
        }

        this.querying = true;

        var $this = this;

        if (this.cfg.itemtip) {
            this.itemtip.hide();
        }

        var options;

        if (!this.cfg.completeEndpoint) {
            options = {
                source: this.id,
                process: this.id,
                update: this.id,
                formId: this.getParentFormId(),
                onsuccess: function (responseXML, status, xhr) {
                    PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function (content) {
                            if (this.cfg.dynamic && !this.isDynamicLoaded) {
                                this.panel = $(content);
                                this.appendPanel();
                                this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
                                content = this.panel.get(0).innerHTML;
                            } else {
                                this.panel.html(content);
                            }

                            if (this.cfg.cache) {
                                if (this.cfg.queryMode !== 'server' && !this.isDynamicLoaded && this.cache[query]) {
                                    this.panel.html(this.cache[query]);
                                } else {
                                    this.cache[query] = content;
                                }
                            }

                            this.showSuggestions(query);
                        }
                    });

                    return true;
                },
                oncomplete: function () {
                    $this.querying = false;
                    $this.isDynamicLoaded = true;
                }
            };

            options.params = [
                {name: this.id + '_query', value: query}
            ];

            if (this.cfg.queryMode === 'hybrid') {
                options.params.push({name: this.id + '_clientCache', value: true});
            }

            if (this.cfg.dynamic && !this.isDynamicLoaded) {
                options.params.push({name: this.id + '_dynamicload', value: true});
            }
        }

        if (this.hasBehavior('query')) {
            this.callBehavior('query', options);
        }
        else {
            if (!!this.cfg.completeEndpoint) {
                $.ajax({
                        url: this.cfg.completeEndpoint,
                        data: { query: query },
                        dataType: 'json'
                    })
                    .done(function(suggestions) {
                        var html = '<ul class="ui-autocomplete-items ui-autocomplete-list ui-widget-content ui-widget ui-corner-all ui-helper-reset">';
                        suggestions.suggestions.forEach(function(suggestion) {
                            var labelEncoded = $("<div>").text(suggestion.label).html();
                            var itemValue = labelEncoded;
                            if (!!suggestion.value) {
                                itemValue = $("<div>").text(suggestion.value).html();
                            }
                            html += '<li class="ui-autocomplete-item ui-autocomplete-list-item ui-corner-all" data-item-value="' + itemValue + '" data-item-label="' + labelEncoded + '" role="option">' + labelEncoded + '</li>';
                        });
                        if (suggestions.moreAvailable == true && $this.cfg.moreText) {
                            var moreTextEncoded = $("<div>").text($this.cfg.moreText).html();
                            html += '<li class="ui-autocomplete-item ui-autocomplete-moretext ui-corner-all" role="option">' + moreTextEncoded + '</li>';
                        }
                        html += '</ul>';

                        $this.panel.html(html);

                        $this.showSuggestions(query);
                    })
                    .always(function() {
                        $this.querying = false;
                    });
            }
            else {
                PrimeFaces.ajax.Request.handle(options);
            }
        }
    },

    /**
     * Shows the panel with the suggestions.
     * @private
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.panel.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.bindPanelEvents();
                }
            });
        }
    },

    /**
     * Hides the panel with the suggestions.
     * @private
     */
    hide: function() {
        if (this.panel.is(':visible') && this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.panel.css('height', 'auto');
                }
            });
        }

        if (this.cfg.itemtip) {
            this.itemtip.hide();
        }
    },

    /**
     * Invokes the appropriate behavior for when a suggestion item was selected.
     * @private
     * @param {string} itemValue Value of the selected item.
     */
    invokeItemSelectBehavior: function(itemValue) {
        if(this.hasBehavior('itemSelect')) {
            var ext = {
                params : [
                    {name: this.id + '_itemSelect', value: itemValue}
                ]
            };

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

    /**
     * Invokes the appropriate behavior when a suggestion item was unselected.
     * @private
     * @param {string} itemValue Value of the unselected item.
     */
    invokeItemUnselectBehavior: function(itemValue) {
        if(this.hasBehavior('itemUnselect')) {
            var ext = {
                params : [
                    {name: this.id + '_itemUnselect', value: itemValue}
                ]
            };

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

    /**
     * Invokes the appropriate behavior for when more text was selected.
     * @private
     */
    invokeMoreTextBehavior: function() {
        if(this.hasBehavior('moreTextSelect')) {
            var ext = {
                params : [
                    {name: this.id + '_moreTextSelect', value: true}
                ]
            };

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

    /**
     * Invokes the appropriate behavior for when empty message was selected.
     * @private
     */
    invokeEmptyMessageBehavior: function() {
        if(this.hasBehavior('emptyMessageSelect')) {
            var ext = {
                params : [
                    {name: this.id + '_emptyMessageSelect', value: true}
                ]
            };

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

    /**
     * Add the given suggestion item.
     * @param {JQuery | string} item Suggestion item to add.
     */
    addItem: function(item) {
        var $this = this,
            itemValue = '',
            itemStyleClass = '',
            itemLabel = '';
 
        if($this.input.hasClass('ui-state-disabled')) {
            return;
        }

        if(typeof item === 'string' || item instanceof String) {
            itemValue = item;
            itemLabel = item;
        }
        else {
            itemValue = item.attr('data-item-value');
            itemLabel = item.attr('data-item-label');
            itemStyleClass = item.attr('data-item-class');
        }

        if (!itemValue) {
            return;
        }

        if ($this.cfg.multiple) {
            var found = false;
            if ($this.cfg.unique) {
                found = $this.multiItemContainer.children("li[data-token-value='" + $.escapeSelector(itemValue) + "']").length != 0;
            }

            if (!found) {
                if ($this.multiItemContainer.children('li.ui-autocomplete-token').length >= $this.cfg.selectLimit) {
                   return;
                }
                var itemDisplayMarkup = '<li data-token-value="' + PrimeFaces.escapeHTML(itemValue);
                itemDisplayMarkup += '"class="ui-autocomplete-token ui-state-active ui-corner-all ui-helper-hidden';
                itemDisplayMarkup += (itemStyleClass === '' ? '' : ' ' + itemStyleClass) + '">';
                itemDisplayMarkup += '<span class="ui-autocomplete-token-icon ui-icon ui-icon-close"></span>';
                itemDisplayMarkup += '<span class="ui-autocomplete-token-label">' + PrimeFaces.escapeHTML(itemLabel) + '</span></li>';

                $this.inputContainer.before(itemDisplayMarkup);
                $this.multiItemContainer.children('.ui-helper-hidden').fadeIn();
                $this.input.val('');
                $this.input.removeAttr('placeholder');

                $this.hinput.append('<option value="' + PrimeFaces.escapeHTML(itemValue) + '" selected="selected"></option>');
                if ($this.multiItemContainer.children('li.ui-autocomplete-token').length >= $this.cfg.selectLimit) {
                    $this.input.css('display', 'none').trigger("blur");
                    $this.disableDropdown();
                }

                $this.invokeItemSelectBehavior(itemValue);
            }
        } else {
            $this.input.val(item.attr('data-item-label'));

            this.currentText = $this.input.val();
            this.previousText = $this.input.val();

            if ($this.cfg.pojo) {
                $this.hinput.val(itemValue);
            }

            if (PrimeFaces.env.isLtIE(10)) {
                var length = $this.input.val().length;
                $this.input.setSelection(length, length);
            }

            $this.invokeItemSelectBehavior(itemValue);
        }

        if ($this.cfg.onChange) {
            $this.cfg.onChange.call(this);
        }

        if (!$this.isTabPressed) {
            $this.input.trigger('focus');
        }
    },

    /**
     * Removes the given suggestion item.
     * @param {JQuery | string} item Suggestion item to remove.
     */
    removeItem: function(item) {
        var $this = this,
            itemValue = '';
        if($this.input.hasClass('ui-state-disabled')) {
            return;
        }

        if(typeof item === 'string' || item instanceof String) {
            itemValue = item;
        }
        else {
            itemValue = item.attr('data-token-value');
        }

        var foundItem = this.multiItemContainer.children("li.ui-autocomplete-token[data-token-value='"+itemValue+"']");
        if(!foundItem.length) {
            return;
        }
        var itemIndex = foundItem.index();
        if(!itemValue || itemIndex === -1) {
            return;
        }

        //remove from options
        this.hinput.children('option').eq(itemIndex).remove();

        //remove from items
        foundItem.fadeOut('fast', function() {
            var token = $(this);
            token.remove();
            $this.invokeItemUnselectBehavior(itemValue);
        });
        
        // if empty return placeholder
        if (this.placeholder && this.hinput.children('option').length === 0) {
            this.input.attr('placeholder', this.placeholder);
        }
    },
    
    /**
     * Removes all items if in multiple mode.
     */
    removeAllItems: function() {
        var $this = this;
        if (this.cfg.multiple && !this.input.val().length) {
            this.multiItemContainer.find('.ui-autocomplete-token').each(function( index ) {
                $this.removeItem($(this));
            });
        }
    },

    /**
     * Sets up the event listener for the blur event to force a selection, when that feature is enabled.
     * @private
     */
    setupForceSelection: function() {
        this.currentItems = [this.input.val()];
        var $this = this;

        this.input.on('blur', function(e) {
            // #5731: do not fire clear event if selecting item
            var fireClearEvent = e.relatedTarget == null || PrimeFaces.escapeClientId(e.relatedTarget.id) !== $this.panelId,
            value = $(this).val(),
            valid = $this.isValid(value, fireClearEvent);

            if($this.cfg.autoSelection && valid && $this.checkMatchedItem && $this.items && !$this.isTabPressed && !$this.itemSelectedWithEnter) {
                var selectedItem = $this.items.filter('[data-item-label="' + $.escapeSelector(value) + '"]');
                if (selectedItem.length) {
                    selectedItem.trigger("click");
                }
            }

            $this.checkMatchedItem = false;
        });
    },

    /**
     * Disables the input field.
     */
    disable: function() {
        this.input.addClass('ui-state-disabled').prop('disabled', true);

        if(this.dropdown.length) {
            this.dropdown.addClass('ui-state-disabled').prop('disabled', true);
        }
    },

    /**
     * Enables the input field.
     */
    enable: function() {
        this.input.removeClass('ui-state-disabled').prop('disabled', false);

        if(this.dropdown.length) {
            this.dropdown.removeClass('ui-state-disabled').prop('disabled', false);
        }
    },

    /**
     * Hides suggested items menu.
     */
    close: function() {
        this.hide();
    },

    /**
     * Deactivates search behavior.
     */
    deactivate: function() {
        this.active = false;
    },

    /**
     * Activates search behavior.
     */
    activate: function() {
        this.active = true;
    },

    /**
     * Aligns (positions) the overlay panel that shows the found suggestions.
     */
    alignPanel: function() {
        var panelWidth = null;

        if(this.cfg.multiple) {
            panelWidth = this.multiItemContainer.outerWidth();
        }
        else {
            if(this.panel.is(':visible')) {
                panelWidth = this.panel.children('.ui-autocomplete-items').outerWidth();
            }
            else {
                this.panel.css({'visibility':'hidden','display':'block'});
                panelWidth = this.panel.children('.ui-autocomplete-items').outerWidth();
                this.panel.css({'visibility':'visible','display':'none'});
            }

            var inputWidth = this.input.outerWidth();
            if(panelWidth < inputWidth) {
                panelWidth = inputWidth;
            }
        }

        if(this.cfg.scrollHeight) {
            var heightConstraint = this.panel.is(':hidden') ? this.panel.height() : this.panel.children().height();
            if(heightConstraint > this.cfg.scrollHeight)
                this.panel.height(this.cfg.scrollHeight);
            else
                this.panel.css('height', 'auto');
        }

        this.panel.css({'left':'',
                        'top':'',
                        'width': panelWidth + 'px',
                        'z-index': PrimeFaces.nextZindex(),
                        'transform-origin': 'center top'
                });

        if(this.panel.parent().is(this.jq)) {
            this.panel.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px',
                'transform-origin': 'center top'
            });
        }
        else {
            this.panel.position({
                    my: this.cfg.myPos
                    ,at: this.cfg.atPos
                    ,of: this.cfg.multiple ? this.jq : this.input
                    ,collision: 'flipfit'
                    ,using: function(pos, directions) {
                        $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                    }
                });
        }
    },

    /**
     * Adds the given text to the ARIA status label element.
     * @private
     * @param {string} text Label text to display.
     */
    displayAriaStatus: function(text) {
        this.status.html('<div>' + PrimeFaces.escapeHTML(text) + '</div>');
    },

    /**
     * Takes the available suggestion items and groups them. 
     * @private
     */
    groupItems: function() {
        var $this = this;

        if(this.items.length) {
            this.itemContainer = this.panel.children('.ui-autocomplete-items');

            var firstItem = this.items.eq(0);
            if(!firstItem.hasClass('ui-autocomplete-moretext')) {
                this.currentGroup = firstItem.data('item-group');
                var currentGroupTooltip = firstItem.data('item-group-tooltip');

                firstItem.before(this.getGroupItem($this.currentGroup, $this.itemContainer, currentGroupTooltip));
            }

            this.items.filter(':not(.ui-autocomplete-moretext)').each(function(i) {
                var item = $this.items.eq(i),
                itemGroup = item.data('item-group'),
                itemGroupTooltip = item.data('item-group-tooltip');

                if($this.currentGroup !== itemGroup) {
                    $this.currentGroup = itemGroup;
                    item.before($this.getGroupItem(itemGroup, $this.itemContainer, itemGroupTooltip));
                }
            });
        }
    },

    /**
     * Creates the grouped suggestion item for the given parameters.
     * @private
     * @param {string} group A group where to look for the item.
     * @param {JQuery} container Container element of the group.
     * @param {string} tooltip Optional tooltip for the group item.
     * @return {JQuery} The newly created group item.
     */
    getGroupItem: function(group, container, tooltip) {
        var element = null;

        if(container.is('.ui-autocomplete-table')) {
            if(!this.colspan) {
                this.colspan = this.items.eq(0).children('td').length;
            }

            element = $('<tr class="ui-autocomplete-group ui-widget-header"><td colspan="' + this.colspan + '">' + group + '</td></tr>');
        }
        else {
            element = $('<li class="ui-autocomplete-group ui-autocomplete-list-item ui-widget-header">' + group + '</li>');
        }

        if(element) {
            element.attr('title', tooltip);
        }

        return element;
    },

    /**
     * Clears the set-timeout timer for the autocomplete search.
     * @private
     */
    deleteTimeout: function() {
        clearTimeout(this.timeout);
        this.timeout = null;
    },

    /**
     * Triggers the behavior for when the input was cleared.
     * @private
     */
    fireClearEvent: function() {
        this.callBehavior('clear');
    },

    /**
     * Checks whether the given value is part of the available suggestion items.
     * @param {string} value A value to check.
     * @param {boolean} [shouldFireClearEvent] `true` if clear event should be fired.
     * @return {boolean | undefined} Whether the given value matches a value in the list of available suggestion items;
     * or `undefined` if {@link AutoCompleteCfg.forceSelection} is set to `false`.
     */
    isValid: function(value, shouldFireClearEvent) {
        if(!this.cfg.forceSelection) {
            return;
        }

        var valid = false;

        for(var i = 0; i < this.currentItems.length; i++) {
            var stripedItem = this.currentItems[i];
            if (stripedItem) {
                stripedItem = stripedItem.replace(/\r?\n/g, '');
            }

            if(stripedItem === value) {
                valid = true;
                break;
            }
        }

        if(!valid) {
            this.input.val('');
            if(!this.cfg.multiple) {
                this.hinput.val('');
            }
            if (shouldFireClearEvent) {
                this.fireClearEvent();
            }
        }

        return valid;
    },
    
    /**
     * Fetches the suggestion items for the current query from the server.
     * @private
     */
    fetchItems: function() {
        var $this = this;
        
        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            global: false,
            params: [{name: this.id + '_clientCache', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        $this.setCache($(content));
                    }
                });

                return true;
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },
    
    /**
     * Adds the suggestions items in the given wrapper to the local cache of suggestion items.
     * @private
     * @param {JQuery} wrapper Wrapper element with the suggestions fetched from the server.
     */
    setCache: function(wrapper) {
        var $this = this,
        items = wrapper.find('.ui-autocomplete-item'),
        prevKey = null;

        if (!this.wrapperStartTag || !this.wrapperEndTag) {
            this.findWrapperTag(wrapper);
        }

        for (var i = 0; i < items.length; i++) {
            var item = items.eq(i),
            key = item.data('item-key');

            this.cache[key] = (this.cache[key] || this.wrapperStartTag) + item.get(0).outerHTML;
            
            if ((prevKey !== null && prevKey !== key) || (i === items.length - 1)) {
                this.cache[prevKey] += $this.wrapperEndTag;
            }
            
            prevKey = key;
        }
    },
    
    /**
     * Finds and sets the wrapper HTML snippets on this instance.
     * @private
     * @param {JQuery} wrapper Wrapper element with the suggestions fetched from the server.
     */
    findWrapperTag: function(wrapper) {
        if (wrapper.is('ul')) {
            this.wrapperStartTag = '<ul class="ui-autocomplete-items ui-autocomplete-list ui-widget-content ui-widget ui-corner-all ui-helper-reset">';
            this.wrapperEndTag = '</ul>';
        }
        else {
            var header = wrapper.find('> table > thead');
            this.wrapperStartTag = '<table class="ui-autocomplete-items ui-autocomplete-table ui-widget-content ui-widget ui-corner-all ui-helper-reset">' +
                    (header.length ? header.eq(0).outherHTML : '') +
                    '<tbody>';
            this.wrapperEndTag = '</tbody></table>';
        }
    },

    /**
     * Clears the input field.
     */
    clear: function() {
        this.input.val('');
        if (this.cfg.multiple) {
            this.removeAllItems();
        }
        else if (this.cfg.pojo) {
            this.hinput.val('');
        }
    }
    
});

/**
 * __PrimeFaces BlockUI Widget__
 * 
 * BlockUI is used to block interactivity of JSF components with optional AJAX integration.
 * 
 * @prop {JQuery} block The DOM element for the overlay that blocks the UI.
 * @prop {JQuery} content The DOM element for the content of the blocker.
 * @prop {JQuery} blocker The DOM element for the content of the blocking overlay. 
 * 
 * @interface {PrimeFaces.widget.BlockUICfg} cfg The configuration for the {@link  BlockUI| BlockUI 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 When disabled, displays block without animation effect.
 * @prop {boolean} cfg.blocked Blocks the UI by default when enabled.
 * @prop {string} cfg.block Search expression for block targets.
 * @prop {string} cfg.styleClass Style class of the component.
 * @prop {string} cfg.triggers Search expression of the components to bind.
 */
PrimeFaces.widget.BlockUI = PrimeFaces.widget.BaseWidget.extend({
    
    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.block = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.block);
        this.content = this.jq;
        this.cfg.animate = (this.cfg.animate === false) ? false : true;
        this.cfg.blocked = (this.cfg.blocked === true) ? true : false;
        
        this.render();

        if(this.cfg.triggers) {
            this.bindTriggers();
        }
        
        if(this.cfg.blocked) {
            this.show();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.blocker.remove();
        this.block.children('.ui-blockui-content').remove();
        $(document).off('pfAjaxSend.' + this.id + ' pfAjaxComplete.' + this.id);
        
        this._super(cfg);
    },
    
    /**
     * Sets up the global event listeners on the document.
     * @private
     */
    bindTriggers: function() {
        var $this = this;
        
        //listen global ajax send and complete callbacks
        $(document).on('pfAjaxSend.' + this.id, function(e, xhr, settings) {
            var sourceId = $.type(settings.source) === 'string' ? settings.source : settings.source.name;
            // we must evaluate it each time as the DOM might has been changed
            var triggers = PrimeFaces.expressions.SearchExpressionFacade.resolveComponents($this.cfg.triggers);
            
            if($.inArray(sourceId, triggers) !== -1 && !$this.cfg.blocked) {
                $this.show();
            }
        });

        $(document).on('pfAjaxComplete.' + this.id, function(e, xhr, settings) {
            var sourceId = $.type(settings.source) === 'string' ? settings.source : settings.source.name;
            // we must evaluate it each time as the DOM might has been changed
            var triggers = PrimeFaces.expressions.SearchExpressionFacade.resolveComponents($this.cfg.triggers);
            
            if($.inArray(sourceId, triggers) !== -1 && !$this.cfg.blocked) {
                $this.hide();
            }
        });
    },
    
    /**
     * Show the component with optional duration animation.
     * 
     * @param {number | string} [duration] Durations are given in milliseconds; higher values indicate slower
     * animations, not faster ones. The strings `fast` and `slow` can be supplied to indicate durations of 200 and 600
     * milliseconds, respectively.
     */
    show: function(duration) {
        this.blocker.css('z-index', PrimeFaces.nextZindex());
        
        //center position of content
        for(var i = 0; i < this.block.length; i++) {
            var blocker = $(this.blocker[i]),
                content = $(this.content[i]);
           
            content.css({
                'left': ((blocker.width() - content.outerWidth()) / 2) + 'px',
                'top': ((blocker.height() - content.outerHeight()) / 2)+ 'px',
                'z-index': PrimeFaces.nextZindex()
            });
        }

        var animated = this.cfg.animate;
        if(animated)
            this.blocker.fadeIn(duration);    
        else
            this.blocker.show(duration);

        if(this.hasContent()) {
            if(animated)
                this.content.fadeIn(duration);
            else
                this.content.show(duration);
        }
        
        this.block.attr('aria-busy', true);
    },
    
    /**
     * Hide the component with optional duration animation.
     * 
     * @param {number} [duration] Durations are given in milliseconds; higher values indicate slower animations, not
     * faster ones. The strings `fast` and `slow` can be supplied to indicate durations of 200 and 600 milliseconds,
     * respectively.
     */
    hide: function(duration) {
        var animated = this.cfg.animate;

        if(animated)
            this.blocker.fadeOut(duration);
        else
            this.blocker.hide(duration);

        if(this.hasContent()) {
            if(animated)
                this.content.fadeOut(duration);
            else
                this.content.hide(duration);
        }
        
        this.block.attr('aria-busy', false);
    },
    
    /**
     * Renders the client-side parts of this widget.
     * @private
     */
    render: function() {   
        this.blocker = $('<div id="' + this.id + '_blocker" class="ui-blockui ui-widget-overlay ui-helper-hidden"></div>');

        if(this.cfg.styleClass) {
            this.blocker.addClass(this.cfg.styleClass);
        }

        if(this.block.hasClass('ui-corner-all')) {
            this.blocker.addClass('ui-corner-all');
        }
        
        if(this.block.length > 1) {
            this.content = this.content.clone();
        }

        var position = this.block.css("position");
        if (position !== "fixed" && position  !== "absolute") {
            this.block.css('position', 'relative');
        }
        this.block.attr('aria-busy', this.cfg.blocked).append(this.blocker).append(this.content);

        if(this.block.length > 1) {
            this.blocker = $(PrimeFaces.escapeClientId(this.id + '_blocker'));
            this.content = this.block.children('.ui-blockui-content');
        }
    },
    
    /**
     * Checks whether the blocking overlay contains any content items.
     * @private
     * @return {boolean} `true` if this blocking overlay has got any content, `false` otherwise.
     */
    hasContent: function() {
        return this.content.contents().length > 0;
    }
    
});

/**
 * __PrimeFaces Calendar Widget__
 *
 * __Deprecated__: Use the {@link DatePicker|p:datePicker} component instead.
 *
 * Calendar is an input component used to select a date featuring display modes, paging, localization, ajax selection
 * and more.
 *
 * To interact with the calendar, use the `timepicker` or `datetimepicker` JQuery plugin, for example:
 *
 * ```javascript
 * PF("calendarWidget").jqEl.datetimepicker("getDate");
 * PF("calendarWidget").jqEl.datetimepicker("setDate", new Date());
 * ```
 *
 * @typedef {"focus" | "button" | "both"} PrimeFaces.widget.Calendar.ShowOnType Client-side event to display the
 * calendar. `focus` is when the input field receives focus. `popup` is when the popup button is clicked. `both` is
 * both `focus` and `popup`.
 *
 * @typedef PrimeFaces.widget.Calendar.PreShowCallback Callback invoked before the calendar is opened.
 * @this {PrimeFaces.widget.Calendar} PrimeFaces.widget.Calendar.PreShowCallback
 * @param {JQuery} PrimeFaces.widget.Calendar.PreShowCallback.input Input element for the date.
 * @param {JQueryUITimepickerAddon.Timepicker} PrimeFaces.widget.Calendar.PreShowCallback.instance Current time picker
 * instance controlling the calendar. `false` to prevent the time picker from being shown.
 * @return {Partial<JQueryUI.DatepickerOptions> | boolean | undefined} PrimeFaces.widget.Calendar.PreShowCallback A new
 * set of options for the time picker.
 *
 * @typedef PrimeFaces.widget.Calendar.PreShowDayCallback Callback invoked before a day is shown.
 * @this {Window} PrimeFaces.widget.Calendar.PreShowDayCallback
 * @param {Date} PrimeFaces.widget.Calendar.PreShowDayCallback.date The current date of the calendar.
 * @return {[boolean, string] | [boolean, string, string]} PrimeFaces.widget.Calendar.PreShowDayCallback Two to three
 * values indicating:
 * 1. true/false indicating whether or not this date is selectable
 * 1. a CSS class name to add to the date's cell or "" for the default presentation
 * 1. an optional popup tooltip for this date
 *
 * @prop {JQuery} input DOM element of the plain-text input field for the date and/or time.
 * @prop {JQuery} jqEl The DOM element on which the JQuery plugin `datepicker` or `datetimepicker` was initialized. You
 * can use this element to interact with the date picker.
 * @prop {boolean} refocusInput Whether the input needs to be refocused.
 *
 * @interface {PrimeFaces.widget.CalendarCfg} cfg The configuration for the {@link  Calendar| Calendar 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 {string} cfg.buttonTabindex Position of the button in the tabbing order.
 * @prop {JQueryUITimepickerAddon.ControlType | "custom"} cfg.controlType How the user selects a time (hour / minute /
 * second). When set to `custom`, the `timeControlObject` must be set.
 * @prop {string} cfg.dateFormat Date format pattern for localization
 * @prop {boolean} cfg.disabled Disables the calendar when set to true.
 * @prop {boolean} cfg.disabledWeekends Disables weekend columns.
 * @prop {string} cfg.duration Duration of the effect.
 * @prop {boolean} cfg.focusOnSelect If enabled, the input is focused again after selecting a date. Default is false.
 * @prop {number} cfg.hour Default for hour selection, if no date is given. Default is 0.
 * @prop {number} cfg.hourMax Maximum boundary for hour selection.
 * @prop {number} cfg.hourMin Minimum boundary for hour selection.
 * @prop {string} cfg.locale Locale to be used for labels and conversion.
 * @prop {string} cfg.mask Applies a mask using the pattern.
 * @prop {boolean} cfg.maskAutoClear Clears the field on blur when incomplete input is entered
 * @prop {string} cfg.maskSlotChar Placeholder in mask template.
 * @prop {string} cfg.maxDate Sets calendar's maximum visible date; Also used for validation on the server-side.
 * @prop {number} cfg.millisec Default for millisecond selection, if no date is given. Default is 0.
 * @prop {string} cfg.minDate Sets calendar's minimum visible date; Also used for validation on the server-side.
 * @prop {number} cfg.minute Default for minute selection, if no date is given. Default is 0.
 * @prop {number} cfg.minuteMax Maximum boundary for hour selection.
 * @prop {number} cfg.minuteMin Minimum boundary for minute selection.
 * @prop {number} cfg.numberOfMonths Enables multiple page rendering.
 * @prop {boolean} cfg.oneLine Try to show the time dropdowns all on one line. This should be used with the
 * `controlType` set to `select`.
 * @prop {boolean} cfg.popup `true` if `mode` is set to `popup`.
 * @prop {PrimeFaces.widget.Calendar.PreShowCallback} cfg.preShow Callback invoked before the calendar is opened.
 * @prop {PrimeFaces.widget.Calendar.PreShowDayCallback} cfg.preShowDay Callback invoked before a day is shown.
 * @prop {boolean} cfg.readonly Makes the calendar readonly when set to true.
 * @prop {number} cfg.second Default for second selection, if no date is given. Default is 0.
 * @prop {number} cfg.secondMax Maximum boundary for second selection.
 * @prop {number} cfg.secondMin Minimum boundary for second selection.
 * @prop {boolean} cfg.selectOtherMonths Enables selection of days belonging to other months.
 * @prop {string} cfg.showAnim Effect to use when displaying and showing the popup calendar.
 * @prop {boolean} cfg.showButtonPanel Visibility of button panel containing today and done buttons.
 * @prop {string} cfg.showHour Whether to show the hour control.
 * @prop {string} cfg.showMillisec Whether to show the millisec control
 * @prop {string} cfg.showMinute Whether to show the minute control.
 * @prop {PrimeFaces.widget.Calendar.ShowOnType} cfg.showOn Client side event that displays the popup calendar.
 * @prop {boolean} cfg.showOtherMonths Displays days belonging to other months.
 * @prop {string} cfg.showSecond Whether to show the second control.
 * @prop {boolean} cfg.showTodayButton Whether to show the `Current Date` button if `showButtonPanel` is rendered.
 * @prop {boolean} cfg.showWeek Displays the week number next to each week.
 * @prop {string} cfg.stepHour Hour steps.
 * @prop {number} cfg.stepMinute Minute steps.
 * @prop {number} cfg.stepSecond Second steps.
 * @prop {JQueryUITimepickerAddon.CustomControl} cfg.timeControlObject When `controlType` is set to `custom`, an
 * object for creating and handling custom controls for the hour / minute / second inputs.
 * @prop {boolean} cfg.timeInput Allows direct input in time field.
 * @prop {boolean} cfg.timeOnly Shows only timepicker without date.
 * @prop {string} cfg.yearRange Year range for the navigator, default is `c-10:c+10`.
 */
PrimeFaces.widget.Calendar = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.input = $(this.jqId + '_input');
        this.jqEl = this.cfg.popup ? this.input : $(this.jqId + '_inline');
        var $this = this;

        //i18n and l7n
        this.configureLocale();

        //events
        this.bindDateSelectListener();
        this.bindViewChangeListener();
        this.bindCloseListener();
        this.applyMask();

        //disabled dates
        this.cfg.beforeShowDay = function(date) {
            if($this.cfg.preShowDay) {
                return $this.cfg.preShowDay(date);
            }
            else if($this.cfg.disabledWeekends) {
                return $.datepicker.noWeekends(date);
            }
            else {
                return [true,''];
            }
        };

        //Setup timepicker
        var hasTimePicker = this.hasTimePicker();
        if(hasTimePicker) {
            this.configureTimePicker();
        }

        // is touch support enabled
        var touchEnabled = PrimeFaces.env.isTouchable(this.cfg) && !this.input.attr("readonly") && this.cfg.showOn && this.cfg.showOn === 'button';

        //Client behaviors, input skinning and z-index
        if(this.cfg.popup) {
            PrimeFaces.skinInput(this.jqEl);

            if(this.cfg.behaviors) {
                PrimeFaces.attachBehaviors(this.jqEl, this.cfg.behaviors);
            }

            this.cfg.beforeShow = function(input, inst) {
                if($this.refocusInput) {
                    $this.refocusInput = false;
                    return false;
                }

                // #4119 do not popup if readonly
                if ($this.cfg.readonly) {
                    return false;
                }

                //display on top
                setTimeout(function() {
                    $('#ui-datepicker-div').addClass('ui-input-overlay').css('z-index', PrimeFaces.nextZindex());

                    if ($this.cfg.showTodayButton === false) {
                        $(input).datepicker("widget").find(".ui-datepicker-current").hide();
                    }

                    $this.alignPanel();
                }, 50);

                // touch support - prevents keyboard popup
                if(touchEnabled) {
                    $(this).prop("readonly", true);
                }

                //user callback
                var preShow = $this.cfg.preShow;
                if(preShow) {
                    return $this.cfg.preShow.call($this, input, inst);
                }
            };

            PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', $('#ui-datepicker-div'), function() {
                $.datepicker._hideDatepicker();
            });
            PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_hide', function() {
                $.datepicker._hideDatepicker();
            });
        }

        // touch support - prevents keyboard popup
        if (touchEnabled) {
            var fireCloseEvent = this.cfg.onClose;
            this.cfg.onClose = function(dateText, inst) {
                $(this).attr("readonly", false);

                if (fireCloseEvent) {
                    fireCloseEvent();
                }
            };
        }

        //Initialize calendar
        if(hasTimePicker) {
            if(this.cfg.timeOnly)
                this.jqEl.timepicker(this.cfg);
            else
                this.jqEl.datetimepicker(this.cfg);
        }
        else {
            this.jqEl.datepicker(this.cfg);
        }

        //extensions
        if(this.cfg.popup && this.cfg.showOn) {
            var triggerButton = this.jqEl.siblings('.ui-datepicker-trigger:button');
            triggerButton.attr('aria-label',PrimeFaces.getAriaLabel('calendar.BUTTON')).attr('aria-haspopup', true).html('').addClass('ui-button ui-widget ui-state-default ui-corner-all ui-button-icon-only')
                        .append('<span class="ui-button-icon-left ui-icon ui-icon-calendar"></span><span class="ui-button-text">ui-button</span>');

            var title = this.jqEl.attr('title');
            if(title) {
                triggerButton.attr('title', title);
            }

            if(this.cfg.disabled || this.readonly) {
                triggerButton.addClass('ui-state-disabled');
            }

            var buttonIndex = this.cfg.buttonTabindex||this.jqEl.attr('tabindex');
            if(buttonIndex) {
                triggerButton.attr('tabindex', buttonIndex);
            }

            PrimeFaces.skinButton(triggerButton);
            $('#ui-datepicker-div').addClass('ui-shadow');
            this.jq.addClass('ui-trigger-calendar');
        }

        //mark target and descandants of target as a trigger for a primefaces overlay
        if(this.cfg.popup) {
            this.jq.data('primefaces-overlay-target', this.id).find('*').data('primefaces-overlay-target', this.id);
        }

        if (!this.cfg.popup && this.cfg.showTodayButton === false) {
            this.jqEl.parent().find(".ui-datepicker-current").hide();
        }

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Initializes the mask on the input if using a mask and not an inline picker.
     * @private
     */
    applyMask: function() {
        if (this.cfg.inline || this.input.prop('readonly')) {
            return;
        }
        if (this.cfg.mask) {
            var maskCfg = {
                placeholder: this.cfg.maskSlotChar||'_',
                clearMaskOnLostFocus: this.cfg.maskAutoClear||true,
                clearIncomplete: this.cfg.maskAutoClear||true,
                autoUnmask: false
            };
            var pattern = new RegExp("m|d|y|h|s", 'i');
            var isAlias = pattern.test(this.cfg.mask);
            if (isAlias) {
                maskCfg.alias = 'datetime';
                maskCfg.inputFormat = this.cfg.mask;
            } else {
                maskCfg.mask = this.cfg.mask;
            }
            this.input.inputmask('remove').inputmask(maskCfg);
        }
    },

    /**
     * Aligns the overlay panel with the date picker according to the current configuration. It is usually positioned
     * next to or below the input field to which it is attached.
     */
    alignPanel: function () {
        if($.datepicker._lastInput && (this.id + '_input') === $.datepicker._lastInput.id) {
            $('#ui-datepicker-div').css({left: '', top: ''}).position({
                my: 'left top'
                , at: 'left bottom'
                , of: this.input
                , collision: 'flipfit'
            });
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        if(cfg.popup && $.datepicker._lastInput && (cfg.id + '_input') === $.datepicker._lastInput.id) {
            $.datepicker._hideDatepicker();
        }

        this._super(cfg);
    },

    /**
     * Sets up the locale so that this calendar is displayed in the configured langauge.
     * @private
     */
    configureLocale: function() {
        var localeSettings = PrimeFaces.getLocaleSettings(this.cfg.locale);

        if(localeSettings) {
            for(var setting in localeSettings) {
                this.cfg[setting] = localeSettings[setting];
            }
        }
    },

    /**
     * Sets up the event listeners for when the user selects a particular date.
     * @private
     */
    bindDateSelectListener: function() {
        var $this = this;

        this.cfg.onSelect = function() {
            if($this.cfg.popup) {
                $this.fireDateSelectEvent();

                if($this.cfg.focusOnSelect) {
                    $this.refocusInput = true;
                    $this.jqEl.trigger('focus');
                    if(!($this.cfg.showOn && $this.cfg.showOn === 'button')) {
                        $this.jqEl.off('click.calendar').on('click.calendar', function() {
                            $(this).datepicker("show");
                        });
                    }

                    setTimeout(function() {
                        $this.refocusInput = false;
                    }, 10);
                }
            }
            else {
                // GitHub #3760 Pass the config settings to TimePicker
                var settingsObj = {
                       settings : $this.cfg
                };
                var newDate = $this.cfg.timeOnly ? '' : $.datepicker.formatDate($this.cfg.dateFormat, $this.getDate(), $.datepicker._getFormatConfig(settingsObj));
                if($this.cfg.timeFormat) {
                   newDate += ' ' + $this.jqEl.find('.ui_tpicker_time_input')[0].value;
                }

                $this.input.val(newDate);
                $this.fireDateSelectEvent();
            }
        };
    },

    /**
     * Triggers the behaviors and event listener for when the user has selected a certain date.
     * @private
     */
    fireDateSelectEvent: function() {
        this.callBehavior('dateSelect');
    },

    /**
     * Sets up the event listeners for when the user switches to a different month or year.
     * @private
     */
    bindViewChangeListener: function() {
        if(this.hasBehavior('viewChange')) {
            var $this = this;
            this.cfg.onChangeMonthYear = function(year, month) {
                $this.fireViewChangeEvent(year, month);
            };
        }
    },

    /**
     * Triggers the behaviors and event listener for when the user has switched to a different month or year.
     * @private
     * @param {number} year New year for which a calendar is shown.
     * @param {number} month New month for which a calendar is shown (0=January).
     */
    fireViewChangeEvent: function(year, month) {
        if(this.hasBehavior('viewChange')) {
            var ext = {
                    params: [
                        {name: this.id + '_month', value: month},
                        {name: this.id + '_year', value: year}
                    ]
            };

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

    /**
     * Sets up the event listeners for when this calendar is closed.
     * @private
     */
    bindCloseListener: function() {
        if(this.hasBehavior('close')) {
            var $this = this;
            this.cfg.onClose = function() {
                $this.fireCloseEvent();
            };
        }
    },

    /**
     * Triggers the `close` event when this calendar is closed.
     * @private
     */
    fireCloseEvent: function() {
        this.callBehavior('close');
    },

    /**
     * Creates and initializes the confiugration options for the time picker.
     * @private
     */
    configureTimePicker: function() {
        var pattern = this.cfg.dateFormat,
        timeSeparatorIndex = pattern.toLowerCase().indexOf('h');

        this.cfg.dateFormat = pattern.substring(0, timeSeparatorIndex - 1);
        this.cfg.timeFormat = pattern.substring(timeSeparatorIndex, pattern.length);

        //ampm
        if(this.cfg.timeFormat.indexOf('TT') != -1) {
            this.cfg.ampm = true;
        }

        // GitHub #4366 pass date and time settings for min/max date
        var timeSettings = {
                settings : this.cfg
        };
        var parseSettings = $.datepicker._getFormatConfig(timeSettings);

        //restraints
        if(this.cfg.minDate) {
            this.cfg.minDate = $.datepicker.parseDateTime(this.cfg.dateFormat, this.cfg.timeFormat, this.cfg.minDate, parseSettings, this.cfg);
        }

        if(this.cfg.maxDate) {
            this.cfg.maxDate = $.datepicker.parseDateTime(this.cfg.dateFormat, this.cfg.timeFormat, this.cfg.maxDate, parseSettings, this.cfg);
        }

        if(!this.cfg.showButtonPanel) {
            this.cfg.showButtonPanel = false;
        }

        if(this.cfg.controlType == 'custom' && this.cfg.timeControlObject) {
            this.cfg.controlType = this.cfg.timeControlObject;
        }

        if(this.cfg.showHour) {
            this.cfg.showHour = (this.cfg.showHour == "true") ? true : false;
        }

        if(this.cfg.showMinute) {
            this.cfg.showMinute = (this.cfg.showMinute == "true") ? true : false;
        }

        if(this.cfg.showSecond) {
            this.cfg.showSecond = (this.cfg.showSecond == "true") ? true : false;
        }

        if(this.cfg.showMillisec) {
            this.cfg.showMillisec = (this.cfg.showMillisec == "true") ? true : false;
        }
    },

    /**
     * Checks whether this calendar lets the user specify a clock time (and not just a date).
     * @return {boolean} `true` when this calendar includes a clock time picker, `false` otherwise.
     */
    hasTimePicker: function() {
        return this.cfg.dateFormat.toLowerCase().indexOf('h') != -1;
    },

    /**
     * Sets the currently selected date of the datepicker.
     * @param {Date | null | undefined} date Date to display, or `null` or `undefined` to clear the date.
     */
    setDate: function(date) {
        this.jqEl.datetimepicker('setDate', date);
    },

    /**
     * Finds the currently selected date.
     * @return {Date | null} The selected date of the calendar, or `null` when no date is selected.
     */
    getDate: function() {
        return this.jqEl.datetimepicker('getDate');
    },

    /**
     * Enables the calendar, so that the user can select a date.
     */
    enable: function() {
        this.jqEl.datetimepicker('enable');
    },

    /**
     * Disables the calendar, so that the user can no longer select any date..
     */
    disable: function() {
        this.jqEl.datetimepicker('disable');
    }

});

/**
 * __PrimeFaces Carousel Widget__
 *
 * Carousel is a multi purpose component to display a set of data or general content with slide effects.
 *
 * @typedef {"fade" | "slide"} PrimeFaces.widget.Carousel.Effect Name of the animation for the carousel widget.
 *
 * @prop {number} columns The number of simultaneously visible items.
 * @prop {JQuery} dropdown The DOM element for the dropdown for selecting the item to show.
 * @prop {number} first 0-based index of the the first items that is shown currently.
 * @prop {JQuery} header The DOM element for the header of the carousel.
 * @prop {JQuery} items The DOM elements for the carousel items.
 * @prop {JQuery} itemsContainer The DOM element for the container of the carousel items.
 * @prop {number} itemsCount The total number of carousel items.
 * @prop {JQuery} nextNav The DOM element for the button to switch to the next carousel item.
 * @prop {number} page The currently displayed page of carousel items.
 * @prop {JQuery} pageLinks The DOM elements for the links to other carousel pages.
 * @prop {JQuery} prevNav The DOM element for the button to switch to the previous carousel item.
 * @prop {JQuery} responsiveDropdown The DOM element for the responsive dropdown for selecting the item show.
 * @prop {JQuery} stateholder The DOM element for the hidden input storing the currently visible carousel items.
 * @prop {string} stateKey The key of the HTML5 Local Storage that stores the current carousel state.
 * @prop {JQuery} toggler The DOM element for the carousel toggler.
 * @prop {JQuery} toggleableContent The DOM element for the toggleable content of the carousel.
 * @prop {JQuery} toggleStateHolder The DOM element for the hidden input with the current toggle state.
 * @prop {number} totalPages The total number of available carousel pages.
 * @prop {JQuery} viewport The DOM element for the viewport of the carousel that shows the carousel items.
 *
 * @interface {PrimeFaces.widget.CarouselCfg} cfg The configuration for the {@link  Carousel| Carousel 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.DeferredWidgetCfg} cfg
 *
 * @prop {number} cfg.autoplayInterval Sets the time in milliseconds to have Carousel start scrolling automatically
 * after being initialized.
 * @prop {number} cfg.breakpoint Breakpoint value in pixels to switch between small and large viewport.
 * @prop {boolean} cfg.circular Sets continuous scrolling
 * @prop {boolean} cfg.collapsed Whether the carousel is initially collapsed.
 * @prop {string} cfg.easing Name of the easing animation.
 * @prop {PrimeFaces.widget.Carousel.Effect} cfg.effect Name of the animation for transitioning between pages.
 * @prop {number} cfg.effectDuration Duration of the animation in milliseconds.
 * @prop {number} cfg.firstVisible 0-based index of the first element to be displayed
 * @prop {number} cfg.numVisible Number of visible items per page
 * @prop {number} cfg.pageLinks Defines the number of page links of paginator.
 * @prop {boolean} cfg.responsive In responsive mode, carousel adjusts its content based on screen size.
 * @prop {boolean} cfg.stateful Whether the state of the carousel is saved between page loads.
 * @prop {number} cfg.toggleSpeed The speed at which the carousel toggles.
 * @prop {boolean} cfg.toggleable Whether the carousel is toggleable.
 * @prop {boolean} cfg.vertical Sets vertical scrolling
 *
 */
PrimeFaces.widget.Carousel = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.viewport = this.jq.children('.ui-carousel-viewport');
        this.itemsContainer = this.viewport.children('.ui-carousel-items');
        this.items = this.itemsContainer.children('li');
        this.itemsCount = this.items.length;
        this.header = this.jq.children('.ui-carousel-header');
        this.prevNav = this.header.children('.ui-carousel-prev-button');
        this.nextNav = this.header.children('.ui-carousel-next-button');
        this.pageLinks = this.header.find('> .ui-carousel-page-links > .ui-carousel-page-link');
        this.dropdown = this.header.children('.ui-carousel-dropdown');
        this.responsiveDropdown = this.header.children('.ui-carousel-dropdown-responsive');
        this.stateholder = $(this.jqId + '_page');

        if(this.cfg.toggleable) {
            this.toggler = $(this.jqId + '_toggler');
            this.toggleStateHolder = $(this.jqId + '_collapsed');
            this.toggleableContent = this.jq.find(' > .ui-carousel-viewport > .ui-carousel-items, > .ui-carousel-footer');
        }

        this.cfg.numVisible = this.cfg.numVisible || 3;
        this.cfg.firstVisible = this.cfg.firstVisible || 0;
        this.columns = this.cfg.numVisible;
        this.first = this.cfg.firstVisible;
        this.cfg.effectDuration = this.cfg.effectDuration || 500;
        this.cfg.circular = this.cfg.circular || false;
        this.cfg.breakpoint = this.cfg.breakpoint || 640;
        this.page = parseInt(this.first / this.columns);
        this.totalPages = Math.ceil(this.itemsCount / this.cfg.numVisible);

        if(this.cfg.stateful) {
            this.stateKey = PrimeFaces.createStorageKey(this.id, 'Carousel');

            this.restoreState();
        }

        this.renderDeferred();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.stopAutoplay();

        this._super(cfg);
    },

    /**
     * @include
     * @override
     * @inheritdoc
     * @protected
     */
    _render: function() {
        this.updateNavigators();
        this.bindEvents();

        if(this.cfg.vertical) {
            this.calculateItemHeights();
        }
        else if(this.cfg.responsive) {
            this.refreshDimensions();
        }
        else {
            this.calculateItemWidths(this.columns);
            this.jq.width(this.jq.width());
            this.updateNavigators();
        }

        if(this.cfg.collapsed) {
            this.toggleableContent.hide();
        }
    },

    /**
     * Calculates the required width of each item, and applies that width.
     * @private
     */
    calculateItemWidths: function() {
        var firstItem = this.items.eq(0);
        if(firstItem.length) {
            var itemFrameWidth = firstItem.outerWidth(true) - firstItem.width();    //sum of margin, border and padding
            this.items.width((this.viewport.innerWidth() - itemFrameWidth * this.columns) / this.columns);
        }
    },

    /**
     * Calculates the required height of each item, and applies that height.
     * @private
     */
    calculateItemHeights: function() {
        var firstItem = this.items.eq(0);
        if(firstItem.length) {
            if(!this.cfg.responsive) {
                this.items.width(firstItem.width());
                this.jq.width(this.jq.width());
                var maxHeight = 0;
                for(var i = 0; i < this.items.length; i++) {
                    var item = this.items.eq(i),
                    height = item.height();

                    if(maxHeight < height) {
                        maxHeight = height;
                    }
                }
                this.items.height(maxHeight);
            }
            var totalMargins = ((firstItem.outerHeight(true) - firstItem.outerHeight()) / 2) * (this.cfg.numVisible);
            this.viewport.height((firstItem.outerHeight() * this.cfg.numVisible) + totalMargins);
            this.updateNavigators();
            this.itemsContainer.css('top', (-1 * (this.viewport.innerHeight() * this.page))+ 'px');
        }
    },

    /**
     * Calculates the proper size for this widget and applies it.
     * @private
     */
    refreshDimensions: function() {
        var win = $(window);
        if(win.width() <= this.cfg.breakpoint) {
            this.columns = 1;
            this.calculateItemWidths(this.columns);
            this.totalPages = this.itemsCount;
            this.responsiveDropdown.show();
            this.pageLinks.hide();
        }
        else {
            this.columns = this.cfg.numVisible;
            this.calculateItemWidths();
            this.totalPages = Math.ceil(this.itemsCount / this.cfg.numVisible);
            this.responsiveDropdown.hide();
            this.pageLinks.show();
        }

        this.page = parseInt(this.first / this.columns);
        this.updateNavigators();
        this.itemsContainer.css('left', (-1 * (this.viewport.innerWidth() * this.page))+ 'px');
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.prevNav.on('click', function() {
            if($this.page !== 0) {
                $this.setPage($this.page - 1);
            }
            else if($this.cfg.circular) {
                $this.setPage($this.totalPages - 1);
            }
        });

        this.nextNav.on('click', function() {
            var lastPage = ($this.page === ($this.totalPages - 1));

            if(!lastPage) {
                $this.setPage($this.page + 1);
            }
            else if($this.cfg.circular) {
                $this.setPage(0);
            }
        });

        if (PrimeFaces.env.isTouchable(this.cfg)) {
            this.itemsContainer.swipe({
                swipeLeft:function(event) {
                    if($this.page === ($this.totalPages - 1)) {
                        if($this.cfg.circular)
                            $this.setPage(0);
                    }
                    else {
                        $this.setPage($this.page + 1);
                    }
                },
                swipeRight: function(event) {
                    if($this.page === 0) {
                        if($this.cfg.circular)
                            $this.setPage($this.totalPages - 1);
                    }
                    else {
                        $this.setPage($this.page - 1);
                    }
                },
                excludedElements: PrimeFaces.utils.excludedSwipeElements()
            });
        }

        if(this.pageLinks.length) {
            this.pageLinks.on('click', function(e) {
                $this.setPage($(this).index());
                e.preventDefault();
            });
        }

        this.header.children('select').on('change', function() {
            $this.setPage(parseInt($(this).val()) - 1);
        });

        if(this.cfg.autoplayInterval) {
            this.cfg.circular = true;
            this.startAutoplay();
        }

        if(this.cfg.responsive) {
            PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', null, function() {
                if($this.cfg.vertical) {
                    $this.calculateItemHeights();
                }
                else {
                    $this.refreshDimensions();
                }
            });
        }

        if(this.cfg.toggleable) {
            this.toggler.on('mouseover.carouselToggler',function() {
                $(this).addClass('ui-state-hover');
            }).on('mouseout.carouselToggler',function() {
                $(this).removeClass('ui-state-hover');
            }).on('click.carouselToggler', function(e) {
                $this.toggle();
                e.preventDefault();
            });
        }
    },

    /**
     * Updates the navigator icons to reflect the current page.
     * @private
     */
    updateNavigators: function() {
        if(!this.cfg.circular) {
            if(this.page === 0) {
                this.prevNav.addClass('ui-state-disabled');
                this.nextNav.removeClass('ui-state-disabled');
            }
            else if(this.page === (this.totalPages - 1)) {
                this.prevNav.removeClass('ui-state-disabled');
                this.nextNav.addClass('ui-state-disabled');
            }
            else {
                this.prevNav.removeClass('ui-state-disabled');
                this.nextNav.removeClass('ui-state-disabled');
            }
        }

        if(this.pageLinks.length) {
            this.pageLinks.filter('.ui-icon-radio-on').removeClass('ui-icon-radio-on');
            this.pageLinks.eq(this.page).addClass('ui-icon-radio-on');
        }

        if(this.dropdown.length) {
            this.dropdown.val(this.page + 1);
        }

        if(this.responsiveDropdown.length) {
            this.responsiveDropdown.val(this.page + 1);
        }
    },

    /**
     * Moves this carousel to the given page.
     * @param {number} p 0-based index of the page to display.
     */
    setPage: function(p) {
        if(p !== this.page && !this.itemsContainer.is(':animated')) {
            var $this = this,
            animationProps = this.cfg.vertical ? {top: -1 * (this.viewport.innerHeight() * p)} : {left: -1 * (this.viewport.innerWidth() * p)};
            animationProps.easing = this.cfg.easing;

            this.itemsContainer.animate(animationProps,
            {
                duration: this.cfg.effectDuration,
                easing: this.cfg.easing,
                complete: function() {
                    $this.page = p;
                    $this.first = $this.page * $this.columns;
                    $this.updateNavigators();
                    $this.stateholder.val($this.page);
                    if($this.cfg.stateful) {
                        $this.saveState();
                    }
                }
            });
        }
    },

    /**
     * Enables autoplay and starts the slideshow.
     */
    startAutoplay: function() {
        var $this = this;

        this.interval = setInterval(function() {
            if($this.page === ($this.totalPages - 1))
                $this.setPage(0);
            else
                $this.setPage($this.page + 1);
        }, this.cfg.autoplayInterval);
    },

    /**
     * Disables autoplay and stops the slideshow.
     */
    stopAutoplay: function() {
        clearInterval(this.interval);
    },

    /**
     * Expands or collapses the content this carousel, depending on whether it is currently collapsed or expanded,
     * respectively.
     */
    toggle: function() {
        if(this.cfg.collapsed) {
            this.expand();
        }
        else {
            this.collapse();
        }

        PrimeFaces.invokeDeferredRenders(this.id);
    },

    /**
     * If enabled, expands the content of this carousel.
     */
    expand: function() {
        this.toggleState(false, 'ui-icon-plusthick', 'ui-icon-minusthick');

        this.slideDown();
    },

    /**
     * If enabled, collapses the content of this carousel.
     */
    collapse: function() {
        this.toggleState(true, 'ui-icon-minusthick', 'ui-icon-plusthick');

        this.slideUp();
    },

    /**
     * Slides up the toggleable content.
     * @private
     */
    slideUp: function() {
        this.toggleableContent.slideUp(this.cfg.toggleSpeed, 'easeInOutCirc');
    },

    /**
     * Slides down the toggleable content.
     * @private
     */
    slideDown: function() {
        this.toggleableContent.slideDown(this.cfg.toggleSpeed, 'easeInOutCirc');
    },

    /**
     * Expands or collapses this carousel as indicated by the given arguments.
     * @private
     * @param {boolean} collapsed `false` to expand, `true` to collapse.
     * @param {string} removeIcon Class of the remove icon
     * @param {string} addIcon Class of the add icon.
     */
    toggleState: function(collapsed, removeIcon, addIcon) {
        this.toggler.children('span.ui-icon').removeClass(removeIcon).addClass(addIcon);
        this.cfg.collapsed = collapsed;
        this.toggleStateHolder.val(collapsed);

        if(this.cfg.stateful) {
            this.saveState();
        }
    },

    /**
     * Restores the state as saved by `saveState` to this carousel.
     * @private
     */
    restoreState: function() {
        var carouselStateAsString = localStorage.getItem(this.stateKey) || "first: null, collapsed: null";
        this.carouselState = PrimeFaces.csp.evalResult('({' + carouselStateAsString + '})');

        this.first = this.carouselState.first||this.first;
        this.page = parseInt(this.first/this.columns);

        this.stateholder.val(this.page);

        if(this.cfg.toggleable && (this.carouselState.collapsed === false || this.carouselState.collapsed === true)) {
            this.cfg.collapsed = !this.carouselState.collapsed;
            this.toggle();
        }
    },

    /**
     * Saves the current state of this carousel (current page etc.) in HTML5 Local Store.
     * @private
     */
    saveState: function() {
        var carouselStateAsString = "first:" + this.first;

        if(this.cfg.toggleable) {
            carouselStateAsString += ", collapsed: " + this.toggleStateHolder.val();
        }

        localStorage.setItem(this.stateKey, carouselStateAsString);
    },

    /**
     * Clears the state as saved by `saveState`.
     * @private
     */
    clearState: function() {
        if(this.cfg.stateful) {
            localStorage.removeItem(this.stateKey);
        }
    }

});
/**
 * __PrimeFaces ConfirmPopup Widget__
 * 
 * ConfirmPopup displays a confirmation overlay displayed relatively to its target.
 *
 * @interface {PrimeFaces.widget.ConfirmPopup.ConfirmPopupMessage} ConfirmPopupMessage Interface for the message that
 * is shown in the confirm popup.
 * @prop {string} ConfirmPopupMessage.message Main content of the popup message.
 * @prop {boolean} ConfirmPopupMessage.escape If `true`, the message is escaped for HTML. If `false`, the message is
 * interpreted as an HTML string.
 * @prop {string} ConfirmPopupMessage.onShow A JavaScript code snippet that is be evaluated before the message is
 * shown.
 * 
 * @prop {JQuery} content The DOM element for the content of the confirm popup.
 * @prop {JQuery} message DOM element of the confirmation message displayed in this confirm popup.
 * @prop {JQuery} icon The DOM element for the message icon.
 *
 * @interface {PrimeFaces.widget.ConfirmPopupCfg} cfg The configuration for the {@link  ConfirmPopup| ConfirmPopup 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.DynamicOverlayWidgetCfg} cfg
 *
 * @typedef PrimeFaces.widget.ConfirmPopup.HideCallback Callback invoked after the popup is hidden.
 * @this {Window} PrimeFaces.widget.ConfirmPopup.HideCallback
 *
 * @prop {string | null} cfg.appendTo The search expression for the element to which the overlay panel should be
 * appended.
 * @prop {boolean} cfg.dismissable When set `true`, clicking outside of the popup hides the overlay.
 * @prop {string} cfg.showEvent Event on target to show the popup.
 * @prop {string} cfg.hideEvent Event on target to hide the popup.
 * @prop {boolean} cfg.global When enabled, confirmPopup becomes a shared for other components that require confirmation.
 */
PrimeFaces.widget.ConfirmPopup = PrimeFaces.widget.DynamicOverlayWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        cfg.dismissable = (cfg.dismissable === false) ? false : true;
        if (!cfg.appendTo && cfg.global) {
            cfg.appendTo = '@(body)';
        }
    
        this._super(cfg);
    
        this.content = this.jq.children('.ui-confirm-popup-content');
        this.message = this.content.children('.ui-confirm-popup-message');
        this.icon = this.content.children('.ui-confirm-popup-icon');

        this.transition = PrimeFaces.utils.registerCSSTransition(this.jq, 'ui-connected-overlay');
    
        this.bindEvents();
    },
    
    /**
     * Sets up all event listeners required by this widget.
     * @protected
     */
    bindEvents: function() {
        var $this = this;
    
        if (this.cfg.global) {
            PrimeFaces.confirmPopup = this;
    
            this.jq.on('click.ui-confirmpopup', '.ui-confirm-popup-yes, .ui-confirm-popup-no', null, function(e) {
                var el = $(this);
    
                if (el.hasClass('ui-confirm-popup-yes') && PrimeFaces.confirmPopupSource) {
                    var id = PrimeFaces.confirmPopupSource.get(0);
                    var js = PrimeFaces.confirmPopupSource.data('pfconfirmcommand');
    
                    PrimeFaces.csp.executeEvent(id, js, e);
    
                    PrimeFaces.confirmPopup.hide();
                    PrimeFaces.confirmPopupSource = null;
                }
                else if (el.hasClass('ui-confirm-popup-no')) {
                    PrimeFaces.confirmPopup.hide();
                    PrimeFaces.confirmPopupSource = null;
                }
    
                e.preventDefault();
            });
        }
    },

    /**
     * Sets up all panel event listeners
     * @param {string | JQuery} [target] Selector or DOM element of the target component that triggers this popup.
     * @private
     */
    bindPanelEvents: function(target) {
        var $this = this;

        //hide overlay when mousedown is at outside of overlay
        if (this.cfg.dismissable) {
            this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.jq,
                function() { return PrimeFaces.confirmPopupSource; },
                function(e, eventTarget) {
                    if (!($this.jq.is(eventTarget) || $this.jq.has(eventTarget).length > 0)) {
                        $this.hide();
                    }
                });
        }
    
        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    
        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', target, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },
    
    /**
     * Makes the popup visible.
     * @param {string | JQuery} [target] Selector or DOM element of the target component that triggers this popup.
     */
    show: function(target) {
        if (this.transition) {
            var $this = this;

            if (typeof target === 'string') {
                target = $(document.querySelector(target));
            }
            else if (!(target instanceof $)) {
                target = $(target);
            }

            this.transition.show({
                onEnter: function() {
                    $this.jq.css('z-index', PrimeFaces.nextZindex());
                    $this.align(target);
                },
                onEntered: function() {
                    $this.bindPanelEvents(target);
                    $this.applyFocus();
                }
            });
        }
    },
    
    /**
     * Hides the popup.
     * @param {PrimeFaces.widget.ConfirmPopup.HideCallback} callback Callback that is invoked after this popup was closed.
     */
    hide: function(callback) {
        var $this = this;

        if (this.transition) {
            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    if (callback) {
                        callback();
                    }
                }
            });
        }
    },
    
    /**
     * Aligns the popup so that it is shown at the correct position.
     * @param {JQuery} [target] Jquery selector that is the target of this popup
     * @private
     */
    align: function(target) {
        if (target) {
            var $this = this;
    
            this.jq.removeClass('ui-confirm-popup-flipped');
    
            this.jq.css({left:'0px', top:'0px', 'transform-origin': 'center top'}).position({
                    my: 'left top'
                    ,at: 'left bottom'
                    ,of: target
                    ,collision: 'flipfit'
                    ,using: function(pos, directions) {
                        var targetOffset = target.offset();
                        var arrowLeft = 0;
    
                        if (pos.left < targetOffset.left) {
                            arrowLeft = targetOffset.left - pos.left;
                        }
                        $this.jq.css('--overlayArrowLeft', arrowLeft + 'px');
    
                        if (pos.top < targetOffset.top) {
                            $this.jq.addClass('ui-confirm-popup-flipped');
                        }
                        else {
                            pos.top += parseFloat($this.jq.css('margin-top'));
                        }
    
                        $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                    }
                });
        }
    },
    
    /**
     * Applies focus to the first focusable element of the content in the popup.
     */
    applyFocus: function() {
        this.jq.find(':not(:submit):not(:button):input:visible:enabled:first').trigger('focus');
    },

    /**
     * Checks whether this popup is opened and visible.
     * @return {boolean} `true` if this popup is currently being shown, `false` otherwise.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },
    
    /**
     * Shows the given message in this confirmation popup.
     * @param {Partial<PrimeFaces.widget.ConfirmPopup.ConfirmPopupMessage>} msg Message to show.
     */
    showMessage: function(msg) {
        PrimeFaces.confirmPopupSource = (typeof(msg.source) === 'string') ? $(PrimeFaces.escapeClientId(msg.source)) : $(msg.source);
        
        var $this = this;
        var beforeShow = function() {
            if (msg.beforeShow) {
                PrimeFaces.csp.eval(msg.beforeShow);
            }
        
            this.icon.removeClass().addClass('ui-confirm-popup-icon');
            if (msg.icon !== 'null') {
                this.icon.addClass(msg.icon);
            }
        
            if (msg.message) {
                if (msg.escape){
                    this.message.text(msg.message);
                }
                else {
                    this.message.html(msg.message);
                }
            }
        };

        if (this.isVisible()) {
            this.hide(function() {
                beforeShow.call($this);
                $this.show(PrimeFaces.confirmPopupSource);
            });
        }
        else {
            beforeShow.call(this);
            this.show(PrimeFaces.confirmPopupSource);
        }
    }
});

/**
 * __PrimeFaces ColumnToggler Widget__
 * 
 * ColumnToggler is a helper component for the data table to toggle visibility of columns.
 * 
 * @prop {JQuery} table Table to which this column toggle is attached.
 * @prop {JQuery} trigger Button that toggles this column toggler.
 * @prop {string} tableId ID of the table to which this column toggle is attached.
 * @prop {boolean} hasFrozenColumn Whether the table to which this column toggle is attached has got any frozen columns.
 * @prop {boolean} hasStickyHeader Whether the table to which this column toggle is attached has got a sticky header.
 * @prop {JQuery} thead The DOM element for the table head of the table to which this column toggle is attached.
 * @prop {JQuery} tbody The DOM element for the table body of the table to which this column toggle is attached.
 * @prop {JQuery} tfoot The DOM element for the table foot of the table to which this column toggle is attached.
 * @prop {number} frozenColumnCount The number of frozen column of table to which this column toggle is attached.
 * @prop {boolean} visible Whether this column toggler is currently displayed.
 * 
 * @interface {PrimeFaces.widget.ColumnTogglerCfg} cfg The configuration for the {@link  ColumnToggler| ColumnToggler 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.DeferredWidgetCfg} cfg
 * 
 * @prop {string} cfg.trigger ID of the button that toggles this column toggler.
 * @prop {string} cfg.datasource ID of the component (table) to which this column toggler is attached. 
 */
PrimeFaces.widget.ColumnToggler = PrimeFaces.widget.DeferredWidget.extend({

	/**
	 * @override
	 * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
	 */
    init: function(cfg) {
        this._super(cfg);
        this.table = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.datasource);
        this.trigger = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.trigger);
        this.tableId = this.table.attr('id');
        this.hasFrozenColumn = this.table.hasClass('ui-datatable-frozencolumn');
        this.hasStickyHeader = this.table.hasClass('ui-datatable-sticky');
        var clientId = PrimeFaces.escapeClientId(this.tableId);

        if(this.hasFrozenColumn) {
            this.thead = $(clientId + '_frozenThead,' + clientId + '_scrollableThead');
            this.tbody = $(clientId + '_frozenTbody,' + clientId + '_scrollableTbody');
            this.tfoot = $(clientId + '_frozenTfoot,' + clientId + '_scrollableTfoot');
            this.frozenColumnCount = this.thead.eq(0).find('th').length;
        }
        else {
            this.thead = $(clientId + '_head');
            this.tbody = $(clientId + '_data');
            this.tfoot = $(clientId + '_foot');
        }
        this.visible = false;

        this.render();
        this.bindEvents();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        var jqs = $('[id=' + cfg.id.replace(/:/g,"\\:") + ']');
        if(jqs.length > 1) {
            $(document.body).children(this.jqId).remove();
        }

        this.widthAligned = false;

        this._super(cfg);
    },

    /**
     * @override
     * @inheritdoc
     */
    render: function() {
        this.columns = this.thead.find('> tr > th:not(.ui-static-column)');
        this.panel = $(PrimeFaces.escapeClientId(this.cfg.id)).attr('role', 'dialog').addClass('ui-columntoggler ui-widget ui-widget-content ui-shadow ui-corner-all')
                .append('<ul class="ui-columntoggler-items" role="group"></ul>').appendTo(document.body);
        this.itemContainer = this.panel.children('ul');

        var stateHolderId = this.tableId + "_columnTogglerState";
        this.togglerStateHolder = $('<input type="hidden" id="' + stateHolderId + '" name="' + stateHolderId + '" autocomplete="off"></input>');
        this.table.append(this.togglerStateHolder);
        this.togglerState = [];

        //items
        for(var i = 0; i < this.columns.length; i++) {
            var column = this.columns.eq(i),
            hidden = column.hasClass('ui-helper-hidden'),
            boxClass = hidden ? 'ui-chkbox-box ui-widget ui-corner-all ui-state-default' : 'ui-chkbox-box ui-widget ui-corner-all ui-state-default ui-state-active',
            iconClass = (hidden) ? 'ui-chkbox-icon ui-icon ui-icon-blank' : 'ui-chkbox-icon ui-icon ui-icon-check',
            columnChildren = column.children('.ui-column-title'),
            columnTitle = columnChildren.text(),
            columnTogglerCheckboxId = this.tableId + "_columnTogglerChkbx" + i;
            
            var label = columnChildren.find('label');
            if (label.length) {
                columnTitle = label.text();
            }

            this.hasPriorityColumns = column.is('[class*="ui-column-p-"]');

            var item = $('<li class="ui-columntoggler-item">' +
                    '<div class="ui-chkbox ui-widget">' +
                    '<div role="checkbox" tabindex="0" aria-checked="'+ !hidden + '" aria-labelledby="'+ columnTogglerCheckboxId + '" class="' + boxClass + '">' +
                    '<span class="' + iconClass + '"></span></div></div>' +
                    '<label id="' + columnTogglerCheckboxId + '">' + PrimeFaces.escapeHTML(columnTitle) + '</label></li>').data('column', column.attr('id'));

            if(this.hasPriorityColumns) {
                var columnClasses = column.attr('class').split(' ');
                for(var j = 0; j < columnClasses.length; j++) {
                    var columnClass = columnClasses[j],
                    pindex = columnClass.indexOf('ui-column-p-');
                    if(pindex !== -1) {
                        item.addClass(columnClass.substring(pindex , pindex + 13));
                    }
                }
            }

            item.appendTo(this.itemContainer);

            this.togglerState.push(column.attr('id') + '_' + !hidden);
        }

        this.togglerStateHolder.val(this.togglerState.join(','));

        //close icon
        this.closer = $('<a href="#" class="ui-columntoggler-close"><span class="ui-icon ui-icon-close"></span></a>')
                .attr('aria-label', PrimeFaces.getAriaLabel('columntoggler.CLOSE')).prependTo(this.panel);

        if(this.panel.outerHeight() > 200) {
            this.panel.height(200);
        }
        this.hide();
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        //trigger
        this.trigger.off('click.ui-columntoggler').on('click.ui-columntoggler', function(e) {
            if($this.visible)
                $this.hide();
            else
                $this.show();
        });

        //checkboxes
        this.itemContainer.find('> .ui-columntoggler-item > .ui-chkbox > .ui-chkbox-box').on('mouseenter.columnToggler', function() {
                $(this).addClass('ui-state-hover');
            })
            .on('mouseleave.columnToggler', function() {
                $(this).removeClass('ui-state-hover');
            })
            .on('click.columnToggler', function(e) {
                $this.toggle($(this));
                e.preventDefault();
            });

        //labels
        this.itemContainer.find('> .ui-columntoggler-item > label').on('click.selectCheckboxMenu', function(e) {
            $this.toggle($(this).prev().children('.ui-chkbox-box'));
            PrimeFaces.clearSelection();
            e.preventDefault();
        });

        //closer
        this.closer.on('click', function(e) {
            $this.hide();
            $this.trigger.trigger('focus');
            e.preventDefault();
        });

        this.bindKeyEvents();

        PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', $this.panel, 
            function() { return $this.trigger; },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    $this.hide();
                }
            });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.panel, function() {
            $this.alignPanel();
        });
    },

    /**
     * Sets up the event listners for keyboard interaction.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this,
        inputs = this.itemContainer.find('> li > div.ui-chkbox > div.ui-chkbox-box');

        this.trigger.on('focus.columnToggler', function() {
            $(this).addClass('ui-state-focus');
        })
        .on('blur.columnToggler', function() {
            $(this).removeClass('ui-state-focus');
        })
        .on('keydown.columnToggler', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.ENTER:
                    if($this.visible)
                        $this.hide();
                    else
                        $this.show();

                    e.preventDefault();
                break;

                case keyCode.TAB:
                    if($this.visible) {
                        $this.itemContainer.children('li:not(.ui-state-disabled):first').find('div.ui-chkbox-box').trigger('focus');
                        e.preventDefault();
                    }
                break;
            };
        });

        inputs.on('focus.columnToggler', function() {
            $(this).addClass('ui-state-focus');
            //PrimeFaces.scrollInView($this.panel, box);
        })
        .on('blur.columnToggler', function(e) {
            $(this).removeClass('ui-state-focus');
        })
        .on('keydown.columnToggler', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.TAB:
                    var index = $(this).closest('li').index();
                    if(e.shiftKey) {
                        if(index === 0)
                            $this.closer.trigger('focus');
                        else
                            inputs.eq(index - 1).trigger('focus');
                    }
                    else {
                        if(index === ($this.columns.length - 1) && !e.shiftKey)
                            $this.closer.trigger('focus');
                        else
                            inputs.eq(index + 1).trigger('focus');
                    }

                    e.preventDefault();
                    break;
                case keyCode.ENTER:
                case keyCode.SPACE:
                    $this.toggle($(this));
                    e.preventDefault();
                    break;
            }
        })
        .on('change.columnToggler', function(e) {
            if($(this).attr('aria-checked') === "true") {
                $this.check(box);
                $(this).removeClass('ui-state-active');
            }
            else {
                $this.uncheck(box);
            }
        });

        this.closer.on('keydown.columnToggler', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER)) {
                $this.hide();
                $this.trigger.trigger('focus');
                e.preventDefault();
            }
            else if(key === keyCode.TAB) {
                if(e.shiftKey)
                    inputs.eq($this.columns.length - 1).trigger('focus');
                else
                    inputs.eq(0).trigger('focus');

                e.preventDefault();
            }
        });
    },

    /**
     * Checks or unchecks the given checkbox for a column, depending on whether it is currently selected. Also shows or
     * hides  the column of the table to which this column toggler is attached.
     * @param {JQuery} chkbox Checkbox (`.ui-chkbox-box`) of a column of this column toggler. 
     */
    toggle: function(chkbox) {
        if(chkbox.hasClass('ui-state-active')) {
            this.uncheck(chkbox);
        }
        else {
            this.check(chkbox);
        }
    },

    /**
     * Checks the given checkbox for a column, so that the column is now selected. Also display the column of the table
     * to which this column toggler is attached.
     * @param {JQuery} chkbox Checkbox (`.ui-chkbox-box`) of a column of this column toggler. 
     */
    check: function(chkbox) {
        chkbox.addClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-check').removeClass('ui-icon-blank');

        var column = $(document.getElementById(chkbox.closest('li.ui-columntoggler-item').data('column'))),
        index = column.index() + 1,
        thead = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.thead.eq(0) : this.thead.eq(1)) : this.thead,
        tbody = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.tbody.eq(0) : this.tbody.eq(1)) : this.tbody,
        tfoot = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.tfoot.eq(0) : this.tfoot.eq(1)) : this.tfoot;

        var rowHeader = thead.children('tr'),
        columnHeader = rowHeader.find('th:nth-child(' + index + ')');

        chkbox.attr('aria-checked', true);
        columnHeader.removeClass('ui-helper-hidden');
        $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).removeClass('ui-helper-hidden');
        tbody.children('tr').find('td:nth-child(' + index + ')').removeClass('ui-helper-hidden');
        tfoot.children('tr').find('td:nth-child(' + index + ')').removeClass('ui-helper-hidden');

        if(this.hasFrozenColumn) {
            var headers = rowHeader.children('th');
            if(headers.length !== headers.filter('.ui-helper-hidden').length) {
                thead.closest('td').removeClass('ui-helper-hidden');
            }

            if(!column.hasClass('ui-frozen-column')) {
                index += this.frozenColumnCount;
            }
        }

        if(this.hasStickyHeader) {
            $(PrimeFaces.escapeClientId(columnHeader.attr('id'))).removeClass('ui-helper-hidden');
        }

        this.changeTogglerState(column, true);
        this.fireToggleEvent(true, (index - 1));
        this.updateColspan();
    },

    /**
     * Unchecks the given checkbox for a column, so that the column is now not selected. Also hides the column of the
     * table to which this column toggler is attached.
     * @param {JQuery} chkbox Checkbox (`.ui-chkbox-box`) of a column of this column toggler. 
     */
    uncheck: function(chkbox) {
        chkbox.removeClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');

        var column = $(document.getElementById(chkbox.closest('li.ui-columntoggler-item').data('column'))),
        index = column.index() + 1,
        thead = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.thead.eq(0) : this.thead.eq(1)) : this.thead,
        tbody = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.tbody.eq(0) : this.tbody.eq(1)) : this.tbody,
        tfoot = this.hasFrozenColumn ? (column.hasClass('ui-frozen-column') ? this.tfoot.eq(0) : this.tfoot.eq(1)) : this.tfoot;

        var rowHeader = thead.children('tr'),
        columnHeader = rowHeader.find('th:nth-child(' + index + ')');

        chkbox.attr('aria-checked', false);
        columnHeader.addClass('ui-helper-hidden');
        $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).addClass('ui-helper-hidden');
        tbody.children('tr').find('td:nth-child(' + index + ')').addClass('ui-helper-hidden');
        tfoot.children('tr').find('td:nth-child(' + index + ')').addClass('ui-helper-hidden');

        if(this.hasFrozenColumn) {
            var headers = rowHeader.children('th');
            if(headers.length === headers.filter(':hidden').length) {
                thead.closest('td').addClass('ui-helper-hidden');
            }

            if(!column.hasClass('ui-frozen-column')) {
                index += this.frozenColumnCount;
            }
        }

        if(this.hasStickyHeader) {
            $(PrimeFaces.escapeClientId(columnHeader.attr('id'))).addClass('ui-helper-hidden');
        }

        this.changeTogglerState(column, false);
        this.fireToggleEvent(false, (index - 1));
        this.updateColspan();
    },

    /**
     * Aligns the overlay panel of this column toggler according to the current widget configuration.
     */
    alignPanel: function() {
        this.panel.css({'left':'', 'top':'', 'z-index': PrimeFaces.nextZindex()}).position({
                            my: 'left top'
                            ,at: 'left bottom'
                            ,of: this.trigger
                        });

        if(this.hasPriorityColumns) {
            if(this.panel.outerWidth() <= this.trigger.outerWidth()) {
                this.panel.css('width','auto');
            }

            this.widthAligned = false;
        }

        if(!this.widthAligned && (this.panel.outerWidth() < this.trigger.outerWidth())) {
            this.panel.width(this.trigger.width());
            this.widthAligned = true;
        }
    },

    /**
     * Brings up this column toggler so that the user can which column to hide or show.
     */
    show: function() {
        this.alignPanel();
        this.panel.show();
        this.visible = true;
        this.trigger.attr('aria-expanded', true);
        this.closer.trigger('focus');
    },

    /**
     * Hides this column toggler.
     */
    hide: function() {
        this.panel.fadeOut('fast');
        this.visible = false;
        this.trigger.attr('aria-expanded', false);
    },

    /**
     * Triggers the events listeners and behaviors when a column was selected or unselected.
     * @param {boolean} visible `true` if the column was selected, `false` otherwise. 
     * @param {number} index Index of the toggled column.
     * @private 
     */
    fireToggleEvent: function(visible, index) {
        if(this.hasBehavior('toggle')) {
            var ext = {
                params: [
                    {name: this.id + '_visibility', value: visible ? 'VISIBLE' : 'HIDDEN'},
                    {name: this.id + '_index', value: index}
                ]
            };

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

    /**
     * Computes the required `colspan` for the rows.
     * @private
     * @return {number} The calculated `colspan` for the rows.
     */
    calculateColspan: function() {
        return this.itemContainer.find('> .ui-columntoggler-item > .ui-chkbox > .ui-chkbox-box.ui-state-active').length;
    },

    /**
     * Updates the `colspan` attribute fo the columns of the given row.
     * @private
     * @param {JQuery} row A row to update.
     * @param {string} colspanValue New value for the `colspan` attribute.
     */
    updateRowColspan: function(row, colspanValue) {
        colspanValue = colspanValue || this.calculateColspan();
        if(colspanValue) {
            row.children('td').removeClass('ui-helper-hidden').attr('colspan', colspanValue);
        }
        else {
            row.children('td').addClass('ui-helper-hidden');
        }
    },

    /**
     * Updates the colspan attributes of the target table of this column toggler. Called after a column was selected or
     * unselected, which resulted in a column of the data table to be shown or hidden.
     * @private
     */    
    updateColspan: function() {
        var emptyRow = this.tbody.children('tr:first');
        if(emptyRow && emptyRow.hasClass('ui-datatable-empty-message')) {
            this.updateRowColspan(emptyRow);
        }
        else {
            var colspanValue = this.calculateColspan(),
                $this = this;
            this.tbody.children('.ui-expanded-row-content').each(function() {
                $this.updateRowColspan($(this), colspanValue);
            });
        }
    },

    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        throw new Error('Unsupported Operation');
    },
    
    /**
     * Selects or unselect a column of this column toggler. Also shows or hides the corresponding colum of the table
     * to which this column toggler is attached.
     * @param {JQuery} column A column element (`LI`) of this column toggler.
     * @param {boolean} isHidden `true` to unselect the column and hide the corresponding table column, or `true`
     * otherwise.
     * @private
     */
    changeTogglerState: function(column, isHidden) {
        if(column && column.length) {
            var stateVal = this.togglerStateHolder.val(),
            columnId = column.attr('id'),
            oldColState = columnId + "_" + !isHidden,
            newColState = columnId + "_" + isHidden;
            this.togglerStateHolder.val(stateVal.replace(oldColState, newColState));
        }
    }

});

/**
 * __PrimeFaces Dashboard Widget__
 * 
 * Dashboard provides a portal like layout with drag & drop based reorder capabilities.
 * 
 * Currently this uses the JQueryUI sortable widget. You can use `$.fn.sortable` to interact with the dashboard
 * programmatically.
 * 
 * ```javascript
 * const widget = PF("MyDashboardWidget");
 * 
 * // When dragged outside the dashboard: Have the items revert to their new positions using a smooth animation
 * widget.jq.find(".ui-dashboard-column").sortable("option", "revert", true);
 * ```
 * 
 * @interface {PrimeFaces.widget.DashboardCfg} cfg The configuration for the {@link  Dashboard| Dashboard 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
 * @extends {JQueryUI.SortableOptions} cfg
 */
PrimeFaces.widget.Dashboard = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.connectWith =  this.jqId + ' .ui-dashboard-column';
        this.cfg.placeholder = 'ui-state-hover';
        this.cfg.forcePlaceholderSize = true;
        this.cfg.revert=false;
        this.cfg.handle='.ui-panel-titlebar';

        this.bindEvents();

        $(this.jqId + ' .ui-dashboard-column').sortable(this.cfg);
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        if(this.hasBehavior('reorder')) {
            this.cfg.update = function(e, ui) {
                if(this === ui.item.parent()[0]) {
                    var itemIndex = ui.item.parent().children().filter(':not(script):visible').index(ui.item),
                    receiverColumnIndex =  ui.item.parent().parent().children().index(ui.item.parent());

                    var ext = {
                        params: [
                            {name: $this.id + '_reordered', value: true},
                            {name: $this.id + '_widgetId', value: ui.item.attr('id')},
                            {name: $this.id + '_itemIndex', value: itemIndex},
                            {name: $this.id + '_receiverColumnIndex', value: receiverColumnIndex}
                        ]
                    };

                    if(ui.sender) {
                        ext.params.push({name: $this.id + '_senderColumnIndex', value: ui.sender.parent().children().index(ui.sender)});
                    }

                    $this.callBehavior('reorder', ext);
                }
            };
        }
    },

    /**
     * Disables this dashboard so that it cannot be modified.
     */
    disable: function () {
        this.jq.addClass('ui-state-disabled');
    },

    /**
     * Enables this dashboard so that it can be modified.
     */
    enable: function () {
        this.jq.removeClass('ui-state-disabled');
    }

});
/**
 * __PrimeFaces DataGrid Widget__
 * 
 * DataGrid displays a collection of data in a grid layout.
 *
 * __DataGrid is deprecated, use DataView instead.__
 * 
 * @deprecated
 * 
 * @prop {JQuery} content DOM element of the content container for the data grid.
 * @prop {PrimeFaces.widget.Paginator} paginator When pagination is enabled: The paginator widget instance used for
 * paging.
 * 
 * @interface {PrimeFaces.widget.DataGridCfg} cfg The configuration for the {@link  DataGrid| DataGrid 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 {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {Partial<PrimeFaces.widget.PaginatorCfg>} cfg.paginator When pagination is enabled: The paginator configuration
 * for the paginator.
 */
PrimeFaces.widget.DataGrid = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.content = $(this.jqId + '_content');

        if(this.cfg.paginator) {
            this.setupPaginator();
        }
    },

    /**
     * Initializes the paginator, called during widget initialization.
     * @private
     */
    setupPaginator: function() {
        var $this = this;
        this.cfg.paginator.paginate = function(newState) {
            $this.handlePagination(newState);
        };

        this.paginator = new PrimeFaces.widget.Paginator(this.cfg.paginator);
        this.paginator.bindSwipeEvents(this.jq, this.cfg);
    },

    /**
     * Handles a pagination event by updating the data grid and invoking the appropriate behaviors.
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new pagination state to apply. 
     */
    handlePagination: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [
                {name: this.id + '_pagination', value: true},
                {name: this.id + '_skipChildren', value: true},
                {name: this.id + '_first', value: newState.first},
                {name: this.id + '_rows', value: newState.rows}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.paginator.cfg.page = newState.page;
                $this.paginator.updateUI();
            }
        };

        if(this.hasBehavior('page')) {
            this.callBehavior('page', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Retrieves the paginator widget used by this data grid for pagination. You can use this widget to switch to a
     * different page programatically.
     * @return {PrimeFaces.widget.Paginator | undefined} The paginator widget, or `undefined` when pagination is not
     * enabled.
     */
    getPaginator: function() {
        return this.paginator;
    }

});
/**
 * __PrimeFaces DataList Widget__
 * 
 * DataList presents a collection of data in list layout with several display types.
 * 
 * __DataList is deprecated, use DataView instead.__
 * 
 * @deprecated
 * 
 * @prop {JQuery} content DOM element of the content container for the data grid.
 * @prop {PrimeFaces.widget.Paginator} paginator When pagination is enabled: The paginator widget instance used for
 * paging.
 * 
 * @interface {PrimeFaces.widget.DataListCfg} cfg The configuration for the {@link  DataList| DataList 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 {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {Partial<PrimeFaces.widget.PaginatorCfg>} cfg.paginator When pagination is enabled: The paginator configuration
 * for the paginator.
 */
PrimeFaces.widget.DataList = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.formId = this.jq.parents('form:first').attr('id');
        this.content = $(this.jqId + '_content');

        if(this.cfg.paginator) {
            this.setupPaginator();
        }
    },

    /**
     * Initializes the paginator, called during widget initialization.
     * @private
     */
    setupPaginator: function() {
        var $this = this;
        this.cfg.paginator.paginate = function(newState) {
            $this.handlePagination(newState);
        };

        this.paginator = new PrimeFaces.widget.Paginator(this.cfg.paginator);
        this.paginator.bindSwipeEvents(this.jq, this.cfg);
    },

    /**
     * Handles a pagination event by updating the data grid and invoking the appropriate behaviors.
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new pagination state to apply. 
     */
    handlePagination: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.cfg.formId,
            params: [
                {name: this.id + '_pagination', value: true},
                {name: this.id + '_skipChildren', value: true},
                {name: this.id + '_first', value: newState.first},
                {name: this.id + '_rows', value: newState.rows}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.paginator.cfg.page = newState.page;
                $this.paginator.updateUI();
            }
        };

        if(this.hasBehavior('page')) {
            this.callBehavior('page', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Retrieves the paginator widget used by this data grid for pagination. You can use this widget to switch to a
     * different page programatically.
     * @return {PrimeFaces.widget.Paginator | undefined} The paginator widget, or `undefined` when pagination is not
     * enabled.
     */
    getPaginator: function() {
        return this.paginator;
    }

});
/**
 * __PrimeFaces DataScroller Widget__
 * 
 * DataScroller displays a collection of data with on demand loading using scrolling.
 * 
 * @typedef {"document" | "inline"} PrimeFaces.widget.DataScroller.Mode Target to listen to for the scroll event.
 * `document` registers a delegated listener on the document element, `inline` registers it on an element of the data
 * scroller.
 * 
 * @typedef {"scroll" | "manual"} PrimeFaces.widget.DataScroller.LoadEvent Defines when more items are loaded by the
 * data scroller. `scroll` loads more items as the user scrolls down the page, `manual` loads more items only when the
 * user click the `more` button. 
 * 
 * @prop {boolean} allLoaded `true` if all items were loaded and there are no more items to be loaded, or `false`
 * otherwise.
 * @prop {JQuery} content DOM element of the container for the content with the data scroller.
 * @prop {JQuery} list DOM element of the list with the data items.
 * @prop {boolean} loading `true` if an AJAX request for loading more items is currently process, or `false` otherwise.
 * @prop {JQuery} loaderContainer DOM element of the container with the `more` button for loading more items.
 * @prop {JQuery} loadStatus DOM element of the status text or icon shown while loading.
 * @prop {JQuery} loadTrigger DOM element of the `more` button for loading more item manually.
 * 
 * @interface {PrimeFaces.widget.DataScrollerCfg} cfg The configuration for the {@link  DataScroller| DataScroller 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 {number} cfg.buffer Percentage height of the buffer between the bottom of the page and the scroll position to
 * initiate the load for the new chunk. For example, a value of `10` means that loading happens after the user has
 * scrolled down to at least `90%` of the viewport height.
 * @prop {number} cfg.chunkSize Number of items to load on each load.
 * @prop {PrimeFaces.widget.DataScroller.LoadEvent} cfg.loadEvent Defines when more items are loaded.
 * @prop {PrimeFaces.widget.DataScroller.Mode} cfg.mode Defines the target to listen for scroll event.
 * @prop {number} cfg.offset Number of additional items currently loaded.
 * @prop {boolean} cfg.startAtBottom `true` to set the scroll position to the bottom initally and load data from the
 * bottom, or `false` otherwise.
 * @prop {number} cfg.totalSize The total number of items that can be displayed.
 * @prop {boolean} cfg.virtualScroll Loads data on demand as the scrollbar gets close to the bottom.
 */
PrimeFaces.widget.DataScroller = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.content = this.jq.children('div.ui-datascroller-content');
        this.list = this.cfg.virtualScroll ? this.content.children('div').children('ul') : this.content.children('ul');
        this.loaderContainer = this.content.children('div.ui-datascroller-loader');
        this.loadStatus = $('<div class="ui-datascroller-loading"></div>');
        this.loading = false;
        this.allLoaded = false;
        this.cfg.offset = 0;
        this.cfg.mode = this.cfg.mode||'document';
        this.cfg.buffer = (100 - this.cfg.buffer) / 100;

        if(this.cfg.loadEvent === 'scroll') {
            this.bindScrollListener();
        }
        else {
            this.loadTrigger = this.loaderContainer.children();
            this.bindManualLoader();
        }
    },

    /**
     * Sets up the event listeners for the scroll event, to load more items on-demand.
     * @private
     */
    bindScrollListener: function() {
        var $this = this;

        if(this.cfg.mode === 'document') {
            var win = $(window),
            doc = $(document),
            $this = this;

            PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_align', function() {
                if (win.scrollTop() >= ((doc.height() * $this.cfg.buffer) - win.height()) && $this.shouldLoad()) {
                    $this.load();
                }
            });
        }
        else {
            this.itemHeight = 0;

            if(this.cfg.virtualScroll) {
                var item = this.list.children('li.ui-datascroller-item');
                if(item) {
                    this.itemHeight = item.outerHeight();
                    this.content.children('div').css('min-height', parseFloat((this.cfg.totalSize * this.itemHeight) + 'px'));
                }

                if (this.cfg.startAtBottom) {
                    var pageHeight = this.itemHeight * this.cfg.chunkSize,
                    virtualListHeight = parseFloat(this.cfg.totalSize * this.itemHeight),
                    viewportHeight = this.content.height(),
                    pageCount = Math.floor(virtualListHeight / pageHeight)||1,
                    page = (this.cfg.totalSize % this.cfg.chunkSize) == 0 ? pageCount - 2 : pageCount - 1,
                    top = (virtualListHeight < viewportHeight) ? (viewportHeight - virtualListHeight) : (Math.max(page, 0) * pageHeight);

                    this.list.css('top', top + 'px');
                    this.content.scrollTop(this.content[0].scrollHeight);
                }
            }
            else if (this.cfg.startAtBottom) {                
                this.content.scrollTop(this.content[0].scrollHeight);
                this.cfg.offset = this.cfg.totalSize > this.cfg.chunkSize ? this.cfg.totalSize - this.cfg.chunkSize : this.cfg.totalSize;
                
                var paddingTop = '0';
                if (this.content.height() > this.list.height()) {
                    paddingTop = (this.getInnerContentHeight() - this.list.outerHeight() - this.loaderContainer.outerHeight());
                }
                
                this.list.css('padding-top', paddingTop + 'px');
            }
            
            this.content.on('scroll', function () {
                if($this.cfg.virtualScroll) {                    
                    var virtualScrollContent = this;
                    
                    clearTimeout($this.scrollTimeout);
                    $this.scrollTimeout = setTimeout(function() {
                        var viewportHeight = $this.content.outerHeight(),
                        listHeight = $this.list.outerHeight() + Math.ceil(viewportHeight - $this.content.height()),
                        pageHeight = $this.itemHeight * $this.cfg.chunkSize,
                        virtualListHeight = parseFloat($this.cfg.totalSize * $this.itemHeight),
                        pageCount = (virtualListHeight / pageHeight)||1;

                        if(virtualScrollContent.scrollTop + viewportHeight > parseFloat($this.list.css('top')) + listHeight || virtualScrollContent.scrollTop < parseFloat($this.list.css('top'))) {
                            var page = Math.floor((virtualScrollContent.scrollTop * pageCount) / (virtualScrollContent.scrollHeight)) + 1;
                            $this.loadRowsWithVirtualScroll(page, function () {
                                $this.list.css('top', ((page - 1) * pageHeight) + 'px');
                            });
                        }
                    }, 200);
                }
                else {                    
                    var scrollTop = this.scrollTop,
                    scrollHeight = this.scrollHeight,
                    viewportHeight = this.clientHeight,
                    shouldLoad = $this.shouldLoad() && ($this.cfg.startAtBottom ?
                                (scrollTop <= (scrollHeight - (scrollHeight * $this.cfg.buffer))) && ($this.cfg.totalSize > $this.cfg.chunkSize)
                                :
                                (scrollTop >= ((scrollHeight * $this.cfg.buffer) - viewportHeight)));
                    if (shouldLoad) {
                        $this.load();
                    }
                }
            });
        }
    },

    /**
     * Loads more items and inserts them into the DOM so that the user can see them.
     * @private
     * @param {number} page The page of the items to load. The items are grouped into pages, each page containts
     * `chunkSize` items. 
     * @param {() => void} callback Callback that is invoked when the new items have been loaded and inserted into the
     * DOM.
     */
    loadRowsWithVirtualScroll: function(page, callback) {
        if(this.virtualScrollActive) {
            return;
        }

        this.virtualScrollActive = true;

        var $this = this,
        first = (page - 1) * this.cfg.chunkSize,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_virtualScrolling', value: true},
                     {name: this.id + '_first', value: first}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        //insert new rows
                        this.updateData(content);
                        callback();
                        this.virtualScrollActive = false;
                    }
                });

                return true;
            },
            oncomplete: function(xhr, status, args) {
                if(typeof args.totalSize !== 'undefined') {
                    $this.cfg.totalSize = args.totalSize;
                }
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },
    
    /**
     * Inserts newly loaded items into the DOM.
     * @private
     * @param {string} data New HTML content of the items to insert. 
     * @param {boolean} [clear] `true` to clear all currently existing items, or `false` otherwise.
     * @param {boolean} [pre] `true` to prepend the items, or `false` or `undefined` to append the items to the list of
     * items.
     */
    updateData: function(data, clear, pre) {
        var empty = (clear === undefined) ? true: clear;

        if(empty)
            this.list.html(data);
        else if (pre)
            this.list.prepend(data);
        else
            this.list.append(data);
    },
    
    /**
     * Sets up the event listeners for the click on the `more` button.
     * @private
     */
    bindManualLoader: function() {
        var $this = this;

        this.loadTrigger.on('click.dataScroller', function(e) {
            $this.load();
            e.preventDefault();
        });
    },

    /**
     * Loads more items from the server. Usually triggered either when the user scrolls down or when they click on the
     * `more` button.
     */
    load: function() {
        this.loading = true;
        this.cfg.offset += (this.cfg.chunkSize * (this.cfg.startAtBottom ? -1 : 1));

        this.loadStatus.appendTo(this.loaderContainer);
        if(this.loadTrigger) {
            this.loadTrigger.hide();
        }

        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_load', value: true},{name: this.id + '_offset', value: this.cfg.offset}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        this.updateData(content, false, $this.cfg.startAtBottom);
                    }
                });

                return true;
            },
            oncomplete: function() {
                if ($this.cfg.offset < 0) {
                    $this.cfg.offset = 0;
                }

                $this.loading = false;
                $this.allLoaded = ($this.cfg.startAtBottom) ? $this.cfg.offset == 0 : ($this.cfg.offset + $this.cfg.chunkSize) >= $this.cfg.totalSize;

                $this.loadStatus.remove();

                if($this.loadTrigger && !$this.allLoaded) {
                    $this.loadTrigger.show();
                }
            }
        };

        if(this.hasBehavior('load')) {
            this.callBehavior('load', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Checks whether more items can be loaded now. Item are not allowed to be loaded when an AJAX request is currently
     * in process, or when all items have been loaded already.
     * @return {boolean} `true` if more items are allowed to be loaded, `false` otherwise.
     */
    shouldLoad: function() {
        return (!this.loading && !this.allLoaded);
    },
            
    /**
     * Finds the height of the content, excluding the padding.
     * @private
     * @return {number} The inner height of the content element.
     */
    getInnerContentHeight: function() {
        return (this.content.innerHeight() - parseFloat(this.content.css('padding-top')) - parseFloat(this.content.css('padding-bottom')));
    }
    
});
/**
 * __PrimeFaces DataTable Widget__
 *
 * DataTable displays data in tabular format.
 *
 * @typedef {number | JQuery} PrimeFaces.widget.DataTable.RowSpecifier Either the 0-based index of a row, or the row
 * element (`TR`) itself.
 *
 * @typedef {"ASCENDING" | "DESCENDING" | "UNSORTED"} PrimeFaces.widget.DataTable.SortOrder The available sort order
 * types for the data table.
 *
 * @typedef {"single" | "multiple"} PrimeFaces.widget.DataTable.CmSelectionMode Indicates whether multiple rows or only
 * a single row of a data table can be selected.
 *
 * @typedef {"radio" | "checkbox"} PrimeFaces.widget.DataTable.SelectionMode Indicates whether rows are selected via
 * radio buttons or via checkboxes.
 *
 * @typedef {"single" | "multiple"} PrimeFaces.widget.DataTable.SortMode Indicates whether a data table can be sorted
 * by multiple columns or only by a single column.
 *
 * @typedef {"single" | "multiple"} PrimeFaces.widget.DataTable.RowExpandMode Indicates whether multiple columns of a
 * data table can be expanded at the same time, or whether other expaned rows should be collapsed when a new row is
 * expanded.
 *
 * @typedef {"eager" | "lazy"} PrimeFaces.widget.DataTable.RowEditMode Indicates whether row editors are loaded eagerly
 * or on-demand.
 *
 * @typedef {"eager" | "lazy"} PrimeFaces.widget.DataTable.CellEditMode Indicates whether cell editors are loaded
 * eagerly or on-demand.
 *
 * @typedef {"expand" | "fit"} PrimeFaces.widget.DataTable.ResizeMode Indicates the resize behavior of columns.
 *
 * @typedef {"new" | "add" | "checkbox"} PrimeFaces.widget.DataTable.RowSelectMode Indicates how rows of a data table
 * may be selected. `new` always unselects other rows, `add` preserves the currently selected rows, and `checkbox` adds
 * a checkbox next to each row.
 *
 * @typedef {"cancel" | "save"} PrimeFaces.widget.DataTable.RowEditAction When a row is editable: whether to `save` the
 * current contents of the row or `cancel` the row edit and discard all changes.
 *
 * @typedef PrimeFaces.widget.DataTable.OnRowClickCallback Callback that is invoked when the user clicks on a row of the
 * data table. 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.DataTable.OnRowClickCallback.event The click event that occurred.
 * @param {JQuery} PrimeFaces.widget.DataTable.OnRowClickCallback.row The TR row that was clicked.
 * 
 * @interface {PrimeFaces.widget.DataTable.RowMeta} RowMeta Describes the meta information of row, such as its index and
 * its row key.
 * @prop {string | undefined} RowMeta.key The unique key of the row. `undefined` when no key was defined for the rows.
 * @prop {number} RowMeta.index The 0-based index of the row in the data table.
 *
 *
 * @interface {PrimeFaces.widget.DataTable.SortMeta} SortMeta Describes a sorting operation of the data table. The
 * items of the data table may be sorted by multiple column, in which case the sorting operation is describes by a list
 * of these objects.
 * @prop {string} SortMeta.col ID of the column to sort by.
 * @prop {-1 | 1} SortMeta.order Whether to sort the items by the column value in an ascending or descending order.
 *
 * @implements {PrimeFaces.widget.ContextMenu.ContextMenuProvider<PrimeFaces.widget.DataTable>}
 *
 * @prop {boolean} allLoadedLiveScroll Whether all available items were  already loaded.
 * @prop {string} ascMessage Localized message for sorting a column in ascending order.
 * @prop {JQuery} bodyTable The DOM element for the body part of the table.
 * @prop {Record<number, string>} cacheMap Cache for the contents of a row. Key is the row index, value the HTML content
 * of the row.
 * @prop {number} cacheRows Number of rows to cache.
 * @prop {JQuery} checkAllToggler DOM element of the container with the `check all` checkbox in the header.
 * @prop {JQuery} checkAllTogglerInput DOM element of the `check all` checkbox in the header.
 * @prop {JQuery} clone Clone of the table header.
 * @prop {boolean} columnWidthsFixed Whether column widths are fixed or may be resized.
 * @prop {boolean} contextMenuClick Whether the context menu was clicked.
 * @prop {PrimeFaces.widget.ContextMenu} contextMenuWidget Widget with the context menu for the data table.
 * @prop {JQuery} currentCell Current cell to be edited.
 * @prop {number | null} cursorIndex 0-based index of row where the the cursor is located.
 * @prop {string} descMessage Localized message for sorting a column in descending order.
 * @prop {JQuery} dragIndicatorBottom DOM element of the icon that indicates a column is draggable.
 * @prop {JQuery} dragIndicatorTop DOM element of the icon that indicates a column is draggable.
 * @prop {number[]} expansionProcess List of row indices to expand.
 * @prop {number} filterTimeout ID as returned by `setTimeout` used during filtering.
 * @prop {JQuery | null} focusedRow DOM element of the currently focused row.
 * @prop {boolean} focusedRowWithCheckbox Whether the focused row includes the checkbox for selecting the row.
 * @prop {JQuery} footerCols The DOM elements for the footer columns.
 * @prop {JQuery} footerTable The DOM elements for the footer table.
 * @prop {JQuery} groupResizers The DOM elements for the resizer button of each group.
 * @prop {boolean} hasColumnGroup Whether the table has any column groups.
 * @prop {JQuery} headerTable The DOM elements for the header table.
 * @prop {JQuery} headers DOM elements for the `TH` headers of this data table.
 * @prop {boolean} incellClick Whether a click occurred inside a table cell.
 * @prop {boolean} isRTL Whether the writing direction is set to right-to-left.
 * @prop {boolean} isRowTogglerClicked Whether a row toggler was clicked.
 * @prop {boolean} liveScrollActive Whether live scrolling is currently active.
 * @prop {boolean} loadingLiveScroll Whether data is currently being loaded due to the live scrolling feature.
 * @prop {boolean} mousedownOnRow Whether a mousedown event occurred on a row.
 * @prop {JQuery} orderStateHolder INPUT element storing the current column / row order.
 * @prop {number | null} originRowIndex The original row index of the row that was clicked.
 * @prop {PrimeFaces.widget.Paginator} paginator When pagination is enabled: The paginator widget instance used for
 * paging.
 * @prop {boolean} percentageScrollHeight The current relative vertical scroll position.
 * @prop {boolean} percentageScrollWidth The current relative horizontal scroll position.
 * @prop {boolean} reflowDD `true` if reflow is enabled, `false` otherwise.
 * @prop {number} relativeHeight The height of the table viewport, relative to the total height, used for scrolling.
 * @prop {string[]} resizableState A list with the current widths for each resizable column.
 * @prop {JQuery} resizableStateHolder INPUT element storing the current widths for each resizable column.
 * @prop {number} resizeTimeout The set-timeout timer ID of the timer used for resizing.
 * @prop {JQuery} resizerHelper The DOM element for the resize helper.
 * @prop {string} rowSelector The CSS selector for the table rows.
 * @prop {string} rowSelectorForRowClick The CSS selector for the table rows that can be clicked.
 * @prop {JQuery} scrollBody The DOM element for the scrollable body of the table.
 * @prop {JQuery} scrollFooter The DOM element for the scrollable body of the table.
 * @prop {JQuery} scrollFooterBox The DOM element for the scrollable footer box of the table.
 * @prop {JQuery} scrollHeader The DOM element for the scrollable header of the table.
 * @prop {JQuery} scrollHeaderBox The DOM element for the scrollable header box of the table.
 * @prop {number} scrollOffset The current scroll position.
 * @prop {JQuery} scrollStateHolder INPUT element storing the current scroll position.
 * @prop {number} scrollTimeout The set-timeout timer ID of the timer used for scrolling.
 * @prop {string} scrollbarWidth CSS attribute for the scrollbar width, eg. `20px`.
 * @prop {string[]} selection List of row keys for the currently selected rows.
 * @prop {string} selectionHolder ID of the INPUT element storing the currently selected rows.
 * @prop {boolean} shouldLiveScroll Whether live scrolling is currently enabled.
 * @prop {Record<string, PrimeFaces.widget.DataTable.SortMeta>} sortMeta Information about how each column is sorted.
 * Key is the column key.
 * @prop {JQuery} sortableColumns DOM elements for the columns that are sortable.
 * @prop {JQuery} stickyContainer The DOM element for the sticky container of the table.
 * @prop {JQuery} tbody DOM element of the `TBODY` element of this data table, if it exists.
 * @prop {JQuery} tfoot DOM element of the `TFOOT` element of this data table, if it exists.
 * @prop {JQuery} thead DOM element of the `THEAD` element of this data table, if it exists.
 * @prop {JQuery} theadClone The DOM element for the cloned table head.
 * @prop {boolean} virtualScrollActive Whether virtual scrolling is currently active.
 *
 *
 * @interface {PrimeFaces.widget.DataTableCfg} cfg The configuration for the {@link  DataTable| DataTable 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.DeferredWidgetCfg} cfg
 *
 * @prop {boolean} cfg.allowUnsorting When true columns can be unsorted upon clicking sort.
 * @prop {string} cfg.cellEditMode Defines the cell edit behavior.
 * @prop {string} cfg.cellSeparator Separator text to use in output mode of editable cells with multiple components.
 * @prop {boolean} cfg.clientCache Caches the next page asynchronously.
 * @prop {boolean} cfg.disableContextMenuIfEmpty `true` to disable the context menu when the data table has got on
 * data row, or `false` otherwise.
 * @prop {boolean} cfg.disabledTextSelection Disables text selection on row click.
 * @prop {boolean} cfg.draggableColumns Columns can be reordered with drag & drop when enabled.
 * @prop {boolean} cfg.draggableRows When enabled, rows can be reordered using drag & drop.
 * @prop {string} cfg.editInitEvent Event that triggers row/cell editing.
 * @prop {PrimeFaces.widget.DataTable.CellEditMode} cfg.editMode Whether rows may be edited as a whole or whether each
 * cell can be edited individually.
 * @prop {boolean} cfg.editable Controls incell editing.
 * @prop {boolean} cfg.expansion `true` if rows are expandable, or `false` otherwise.
 * @prop {boolean} cfg.filter `true` if filtering is enabled, or `false` otherwise.
 * @prop {number} cfg.filterDelay Delay for filtering in milliseconds.
 * @prop {string} cfg.filterEvent Event to invoke filtering for input filters.
 * @prop {string} cfg.formId Client ID of the form that is used for AJAX requests.
 * @prop {number} cfg.frozenColumns The number of frozen columns.
 * @prop {boolean} cfg.liveResize Columns are resized live in this mode without using a resize helper.
 * @prop {boolean} cfg.liveScroll Enables live scrolling.
 * @prop {number} cfg.liveScrollBuffer Percentage of the height of the buffer between the bottom of the page and the
 * scroll position to initiate the load for the new chunk. This value is in the range `0...100`.
 * @prop {boolean} cfg.multiSort `true` if sorting by multiple columns is enabled, or `false` otherwise.
 * @prop {boolean} cfg.multiViewState Whether multiple resize mode is enabled.
 * @prop {boolean} cfg.nativeElements `true` to use native radio button and checkbox elements, or `false` otherwise.
 * @prop {PrimeFaces.widget.DataTable.OnRowClickCallback} cfg.onRowClick Callback that is invoked when the user clicked on
 * a row of the data table.
 * @prop {boolean} cfg.reflow Reflow mode is a responsive mode to display columns as stacked depending on screen size.
 * @prop {boolean} cfg.resizableColumns Enables column resizing.
 * @prop {PrimeFaces.widget.DataTable.ResizeMode} cfg.resizeMode Defines the resize behavior.
 * @prop {string} cfg.rowDragSelector CSS selector for the draggable handle.
 * @prop {PrimeFaces.widget.DataTable.RowEditMode} cfg.rowEditMode Defines the row edit.
 * @prop {PrimeFaces.widget.DataTable.RowExpandMode} cfg.rowExpandMode Defines row expand mode.
 * @prop {boolean} cfg.rowHover Adds hover effect to rows. Hover is always on when selection is enabled.
 * @prop {PrimeFaces.widget.DataTable.RowSelectMode} cfg.rowSelectMode Defines row selection mode for multiple
 * selection.
 * @prop {string} cfg.rowSelector CSS selector find finding the rows of this data table.
 * @prop {boolean} cfg.saveOnCellBlur Saves the changes in cell editing on blur, when set to false changes are
 * discarded.
 * @prop {string} cfg.scrollHeight Scroll viewport height.
 * @prop {number} cfg.scrollLimit Maximum number of rows that may be loaded via live scrolling.
 * @prop {number} cfg.scrollStep Number of additional rows to load in each live scroll.
 * @prop {string} cfg.scrollWidth Scroll viewport width.
 * @prop {boolean} cfg.scrollable Makes data scrollable with fixed header.
 * @prop {PrimeFaces.widget.DataTable.SelectionMode} cfg.selectionMode Enables row selection.
 * @prop {boolean} cfg.selectionPageOnly When using a paginator and selection mode is `checkbox`, the select all
 * checkbox in the header will select all rows on the current page if `true`, or all rows on all pages if `false`.
 * Default is `true`.
 * @prop {boolean} cfg.sorting `true` if sorting is enabled on the data table, `false` otherwise.
 * @prop {string[]} cfg.sortMetaOrder IDs of the columns by which to order. Order by the first column, then by the
 * second, etc.
 * @prop {boolean} cfg.stickyHeader Sticky header stays in window viewport during scrolling.
 * @prop {string} cfg.stickyTopAt Selector to position on the page according to other fixing elements on the top of the
 * table.
 * @prop {string} cfg.tabindex The value of the `tabindex` attribute for this data table.
 * @prop {boolean} cfg.allowUnsorting When true columns can be unsorted upon clicking sort.
 * @prop {boolean} cfg.virtualScroll Loads data on demand as the scrollbar gets close to the bottom. 
 *
 * @interface {PrimeFaces.widget.DataTable.WidthInfo} WidthInfo Describes the width information of a DOM element.
 * @prop {number | string} WidthInfo.width The width of the element. It's either a unitless numeric pixel value or a
 * string containing the width including an unit.
 * @prop {boolean} WidthInfo.isOuterWidth Tells whether the width includes the border-box or not. 
 */
PrimeFaces.widget.DataTable = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * Map between the sort order names and the multiplier for the comparator.
     * @protected
     * @type {Record<PrimeFaces.widget.DataTable.SortOrder, -1 | 0 | 1>}
     */
    SORT_ORDER: {
        ASCENDING: 1,
        DESCENDING: -1,
        UNSORTED: 0
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.thead = this.getThead();
        this.tbody = this.getTbody();
        this.tfoot = this.getTfoot();

        if(this.cfg.paginator) {
            this.bindPaginator();
        }

        if(this.cfg.sorting) {
            this.bindSortEvents();
        }

        if(this.cfg.rowHover) {
            this.setupRowHover();
        }

        if(this.cfg.selectionMode) {
            this.setupSelection();
        }

        if(this.cfg.filter) {
            this.setupFiltering();
        }

        if(this.cfg.expansion) {
            this.expansionProcess = [];
            this.bindExpansionEvents();
        }

        if(this.cfg.editable) {
            this.bindEditEvents();
        }

        if(this.cfg.draggableRows) {
            this.makeRowsDraggable();
        }

        if(this.cfg.reflow) {
            this.initReflow();
        }

        if(this.cfg.resizableColumns) {
            this.resizableStateHolder = $(this.jqId + '_resizableColumnState');
            this.resizableState = [];

            if(this.resizableStateHolder.attr('value')) {
                this.resizableState = this.resizableStateHolder.val().split(',');
            }
        }

        this.updateEmptyColspan();
        this.renderDeferred();
    },

    /**
     * @include
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        this.isRTL = this.jq.hasClass('ui-datatable-rtl');

        if(this.cfg.scrollable) {
            this.setupScrolling();
        }

        if(this.cfg.groupColumnIndexes) {
            this.groupRows();
            this.bindToggleRowGroupEvents();
        }

        if(this.cfg.resizableColumns) {
            this.setupResizableColumns();
        }

        if(this.cfg.draggableColumns) {
            this.setupDraggableColumns();
        }

        if(this.cfg.stickyHeader) {
            this.setupStickyHeader();
        }

        if(this.cfg.onRowClick) {
            this.bindRowClick();
        }

        if(this.cfg.expansion) {
            this.initRowExpansion();
            this.updateExpandedRowsColspan();
        }
        if(this.cfg.reflow) {
           this.jq.css('visibility', 'visible');
        }
    },

    /**
     * Retrieves the table header of this data table.
     * @return {JQuery} DOM element of the table header.
     */
    getThead: function() {
        return $(this.jqId + '_head');
    },

    /**
     * Retrieves the table body of this data table.
     * @return {JQuery} DOM element of the table body.
     */
    getTbody: function() {
        return $(this.jqId + '_data');
    },

    /**
     * Retrieves the table footer of this data table.
     * @return {JQuery} DOM element of the table footer.
     */
    getTfoot: function() {
        return $(this.jqId + '_foot');
    },

    /**
     * Sets the given HTML string as the content of the body of this data table. Afterwards, sets up all required event
     * listeners etc.
     * @protected
     * @param {string} data HTML string to set on the body.
     * @param {boolean} [clear] Whether the contents of the table body should be removed beforehand.
     */
    updateData: function(data, clear) {
        var empty = (clear === undefined) ? true: clear;

        if(empty)
            this.tbody.html(data);
        else
            this.tbody.append(data);

        this.postUpdateData();
    },

    /**
     * Called after an AJAX update. Binds the appropriate event listeners again.
     * @private
     */
    postUpdateData: function() {
        if(this.cfg.draggableRows) {
            this.makeRowsDraggable();
        }

        if(this.cfg.reflow) {
            this.initReflow();
        }

        if(this.cfg.groupColumnIndexes) {
            this.groupRows();
            this.bindToggleRowGroupEvents();
        }

        if(this.cfg.expansion) {
            this.initRowExpansion();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.columnWidthsFixed = false;

        this.unbindEvents();

        this._super(cfg);
    },

    /**
     * Removes event listeners needed if refreshing to prevent multiple sort and pagination events.
     *
     * Cancels all current drag and drop events.
     * @private
     */
    unbindEvents: function() {
        if (this.sortableColumns) {
            this.sortableColumns.off();
        }
        if (this.paginator) {
            this.paginator.unbindEvents();
        }

        // #5582: destroy any current draggable items
        if (this.cfg.draggableColumns || this.cfg.draggableRows) {
            var dragdrop = $.ui.ddmanager.current;
            if (dragdrop && dragdrop.helper) {
                var item = dragdrop.currentItem || dragdrop.element;
                if(item.closest('.ui-datatable')[0] === this.jq[0]) {
                    document.body.style.cursor = 'default';
                    dragdrop.cancel();
                }
            }
        }
    },

    /**
     * Binds the change event listener and renders the paginator
     * @private
     */
    bindPaginator: function() {
        var _self = this;
        this.cfg.paginator.paginate = function(newState) {
            if(_self.cfg.clientCache) {
                _self.loadDataWithCache(newState);
            }
            else {
                _self.paginate(newState);
            }
        };

        this.paginator = new PrimeFaces.widget.Paginator(this.cfg.paginator);
        this.paginator.bindSwipeEvents(this.jq, this.cfg);

        if(this.cfg.clientCache) {
            this.cacheRows = this.paginator.getRows();
            var newState = {
                first:  this.paginator.getFirst(),
                rows: this.paginator.getRows(),
                page: this.paginator.getCurrentPage()
            };
            this.clearCacheMap();
            this.fetchNextPage(newState);
        }
    },

    /**
     * Applies events related to sorting in a non-obstrusive way
     * @private
     */
    bindSortEvents: function() {
        var $this = this,
            hasAriaSort = false;
        this.cfg.tabindex = this.cfg.tabindex||'0';
        this.cfg.multiSort = this.cfg.multiSort||false;
        this.cfg.allowUnsorting = this.cfg.allowUnsorting||false;
        this.headers = this.thead.find('> tr > th');
        this.sortableColumns = this.headers.filter('.ui-sortable-column');
        this.sortableColumns.attr('tabindex', this.cfg.tabindex);

        //aria messages
        this.ascMessage = PrimeFaces.getAriaLabel('datatable.sort.SORT_ASC');
        this.descMessage = PrimeFaces.getAriaLabel('datatable.sort.SORT_DESC');
        this.otherMessage = PrimeFaces.getAriaLabel('datatable.sort.SORT_LABEL');

        //reflow dropdown
        this.reflowDD = $(this.jqId + '_reflowDD');

        this.sortMeta = [];

        for(var i = 0; i < this.sortableColumns.length; i++) {
            var columnHeader = this.sortableColumns.eq(i),
            columnHeaderId = columnHeader.attr('id'),
            sortIcon = columnHeader.children('span.ui-sortable-column-icon'),
            sortOrder = null,
            resolvedSortMetaIndex = null,
            ariaLabel = columnHeader.attr('aria-label');

            if (columnHeader.hasClass('ui-state-active')) {
                if (sortIcon.hasClass('ui-icon-triangle-1-n')) {
                    sortOrder = this.SORT_ORDER.ASCENDING;
                    columnHeader.attr('aria-label', this.getSortMessage(ariaLabel, this.descMessage));
                    if (!hasAriaSort) {
                        columnHeader.attr('aria-sort', 'ascending');
                        hasAriaSort = true;
                    }
                }
                else if (sortIcon.hasClass('ui-icon-triangle-1-s')) {
                    sortOrder = this.SORT_ORDER.DESCENDING;
                    columnHeader.attr('aria-label', this.getSortMessage(ariaLabel, this.otherMessage));
                    if (!hasAriaSort) {
                        columnHeader.attr('aria-sort', 'descending');
                        hasAriaSort = true;
                    }
                } else {
                    sortOrder = this.SORT_ORDER.UNSORTED;
                    columnHeader.attr('aria-label', this.getSortMessage(ariaLabel, this.ascMessage));
                    if (!hasAriaSort) {
                        columnHeader.attr('aria-sort', 'other');
                        hasAriaSort = true;
                    }
                }

                if (this.cfg.multiSort && this.cfg.sortMetaOrder) {
                    resolvedSortMetaIndex = $.inArray(columnHeaderId, this.cfg.sortMetaOrder);

                    this.sortMeta[resolvedSortMetaIndex] = {
                        col: columnHeaderId,
                        order: sortOrder
                    };
                }

                $this.updateReflowDD(columnHeader, sortOrder);
            }
            else {
                sortOrder = this.SORT_ORDER.UNSORTED;
                columnHeader.attr('aria-label', this.getSortMessage(ariaLabel, this.ascMessage));
                if(!hasAriaSort && i == (this.sortableColumns.length - 1)) {
                    this.sortableColumns.eq(0).attr('aria-sort', 'other');
                    hasAriaSort = true;
                }
            }

            columnHeader.data('sortorder', sortOrder);
        }

        this.sortableColumns.on('mouseenter.dataTable', function() {
            var column = $(this);
            column.addClass('ui-state-hover');
        })
        .on('mouseleave.dataTable', function() {
            var column = $(this);
            column.removeClass('ui-state-hover');
        })
        .on('blur.dataTable', function() {
            $(this).removeClass('ui-state-focus');
        })
        .on('focus.dataTable', function() {
            $(this).addClass('ui-state-focus');
        })
        .on('keydown.dataTable', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER) && $(e.target).is(':not(:input)')) {
                $(this).trigger('click.dataTable', (e.metaKey||e.ctrlKey));
                e.preventDefault();
            }
        })
        .on('click.dataTable', function(e, metaKeyOn) {
            if(!$this.shouldSort(e, this)) {
                return;
            }

            PrimeFaces.clearSelection();

            var columnHeader = $(this),
                sortOrderData = columnHeader.data('sortorder'),
                sortOrder = (sortOrderData === $this.SORT_ORDER.UNSORTED) ? $this.SORT_ORDER.ASCENDING :
                    (sortOrderData === $this.SORT_ORDER.ASCENDING) ? $this.SORT_ORDER.DESCENDING :
                        $this.cfg.allowUnsorting ? $this.SORT_ORDER.UNSORTED : $this.SORT_ORDER.ASCENDING,
                metaKey = e.metaKey || e.ctrlKey || metaKeyOn;

            if(!$this.cfg.multiSort || !metaKey) {
                $this.sortMeta = [];
            }

            $this.addSortMeta({
                col: columnHeader.attr('id'),
                order: sortOrder
            });

            $this.sort(columnHeader, sortOrder, $this.cfg.multiSort && metaKey);

            if($this.cfg.scrollable) {
                $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).trigger('focus');
            }

            $this.updateReflowDD(columnHeader, sortOrder);
        });

        $this.updateSortPriorityIndicators();

        if(this.reflowDD && this.cfg.reflow) {
            PrimeFaces.skinSelect(this.reflowDD);
            this.reflowDD.on('change', function(e) {
                var arrVal = $(this).val().split('_'),
                    columnHeader = $this.sortableColumns.eq(parseInt(arrVal[0])),
                    sortOrder = parseInt(arrVal[1]);

                    columnHeader.data('sortorder', sortOrder);
                    columnHeader.trigger('click.dataTable');
            });
        }
    },

    /**
     * Creates the sort order message shown to indicate what the current sort order is.
     * @private
     * @param {string | undefined} ariaLabel Optional label text from an aria attribute.
     * @param {string} sortOrderMessage Sort order message.
     * @return {string} The sort order message to use.
     */
    getSortMessage: function(ariaLabel, sortOrderMessage) {
        var headerName = ariaLabel ? ariaLabel.split(':')[0] : '';
        return headerName + ': ' + sortOrderMessage;
    },

    /**
     * Called in response to a click. Checks whether this data table should now be sorted. Returns `false` when there
     * are no items to be sorted, or when no sorting button was clicked.
     * @private
     * @param {JQuery.TriggeredEvent} event (Click) event that occurred.
     * @param {JQuery} column Column Column of this data table on which the event occurred.
     * @return {boolean} `true` to perform a sorting operation, `false` otherwise.
     */
    shouldSort: function(event, column) {
        if(this.isEmpty()) {
            return false;
        }

        var target = $(event.target);
        if(target.closest('.ui-column-customfilter', column).length) {
            return false;
        }

        return target.is('th,span');
    },

    /**
     * Adds the given sorting to the list of sortings. Each sorting describes a column by which to sort. This data table
     * may be sorted by multiple columns.
     * @param {PrimeFaces.widget.DataTable.SortMeta} meta Sorting to add.
     * @private
     */
    addSortMeta: function(meta) {
        this.sortMeta = $.grep(this.sortMeta, function(value) {
            return value.col !== meta.col;
        });

        this.sortMeta.push(meta);
    },

    /**
     * Binds filter events to standard filters
     * @private
     */
    setupFiltering: function() {
        var $this = this,
        filterColumns = this.thead.find('> tr > th.ui-filter-column');
        this.cfg.filterEvent = this.cfg.filterEvent||'keyup';
        this.cfg.filterDelay = this.cfg.filterDelay||300;

        filterColumns.children('.ui-column-filter').each(function() {
            var filter = $(this);

            if(filter.is('input:text')) {
                PrimeFaces.skinInput(filter);
                $this.bindTextFilter(filter);
            }
            else {
                PrimeFaces.skinSelect(filter);
                $this.bindChangeFilter(filter);
            }
        });
    },

    /**
     * Sets up the event listeners for the text filters on a column.
     * @private
     * @param {JQuery} filter INPUT element of the text filter.
     */
    bindTextFilter: function(filter) {
        if(this.cfg.filterEvent === 'enter')
            this.bindEnterKeyFilter(filter);
        else
            this.bindFilterEvent(filter);
    },

    /**
     * Sets up the change event listeners on the column filter elements.
     * @private
     * @param {JQuery} filter DOM element of a column filter
     */
    bindChangeFilter: function(filter) {
        var $this = this;

        filter.off('change')
        .on('change', function() {
            $this.filter();
        });
    },

    /**
     * Sets up the enter key event listeners for the text filters on a column.
     * @private
     * @param {JQuery} filter INPUT element of the text filter.
     */
    bindEnterKeyFilter: function(filter) {
        var $this = this;

        filter.off('keydown keyup')
        .on('keydown', PrimeFaces.utils.blockEnterKey)
        .on('keyup', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER)) {
                $this.filter();

                e.preventDefault();
            }
        });
    },

    /**
     * Sets up all event listeners for the given filter element of a column filter.
     * @private
     * @param {JQuery} filter DOM element of a column filter.
     */
    bindFilterEvent: function(filter) {
        var $this = this;
        var filterEventName = this.cfg.filterEvent + '.dataTable';

        //prevent form submit on enter key
        filter.off('keydown.dataTable-blockenter ' + filterEventName)
        .on('keydown.dataTable-blockenter', PrimeFaces.utils.blockEnterKey)
        .on(filterEventName, function(e) {
            if (PrimeFaces.utils.ignoreFilterKey(e)) {
                return;
            }

            if($this.filterTimeout) {
                clearTimeout($this.filterTimeout);
            }

            $this.filterTimeout = setTimeout(function() {
                $this.filter();
                $this.filterTimeout = null;
            },
            $this.cfg.filterDelay);
        });

        // #89 IE clear "x" button
        if (PrimeFaces.env.isIE()) {
            filter.off('mouseup.dataTable').on('mouseup.dataTable', function(e) {
                var input = $(this),
                oldValue = input.val();

                if(oldValue == "") {
                    return;
                }

                setTimeout(function() {
                    var newValue = input.val();
                    if(newValue == "") {
                        $this.filter();
                    }
                }, 1);
            });
        }
    },

    /**
     * Sets up the data table and adds all event listeners required for hovering over rows.
     * @private
     */
    setupRowHover: function() {
        var selector = '> tr.ui-widget-content';
        if(!this.cfg.selectionMode || this.cfg.selectionMode === 'checkbox') {
            this.bindRowHover(selector);
        }
    },

    /**
     * Sets up the data table and adds all event listener required for selecting rows.
     * @private
     */
    setupSelection: function() {
        this.selectionHolder = this.jqId + '_selection';
        this.cfg.rowSelectMode = this.cfg.rowSelectMode||'new';
        this.rowSelector = '> tr.ui-widget-content.ui-datatable-selectable';
        this.cfg.disabledTextSelection = this.cfg.disabledTextSelection === false ? false : true;
        this.cfg.selectionPageOnly = this.cfg.selectionPageOnly === false ? !this.cfg.paginator : true;
        this.rowSelectorForRowClick = this.cfg.rowSelector||'td:not(.ui-column-unselectable),span:not(.ui-c)';

        var preselection = $(this.selectionHolder).val();
        this.selection = !preselection ? [] : preselection.split(',');

        //shift key based range selection
        this.originRowIndex = null;
        this.cursorIndex = null;

        this.bindSelectionEvents();
    },

    /**
     * Applies events related to selection in a non-obstrusive way
     * @private
     */
    bindSelectionEvents: function() {
        if(this.cfg.selectionMode === 'radio') {
            this.bindRadioEvents();
            this.bindRowEvents();
        }
        else if(this.cfg.selectionMode === 'checkbox') {
            this.bindCheckboxEvents();
            this.updateHeaderCheckbox();

            if(this.cfg.rowSelectMode !== 'checkbox') {
                this.bindRowEvents();
            }
        }
        else {
            this.bindRowEvents();
        }
    },

    /**
     * Sets up all event listeners for event triggered on a row of this data table.
     * @private
     */
    bindRowEvents: function() {
        var $this = this;

        this.bindRowHover(this.rowSelector);

        this.tbody.off('click.dataTable mousedown.dataTable', this.rowSelector).on('mousedown.dataTable', this.rowSelector, null, function(e) {
            $this.mousedownOnRow = true;
        })
        .on('click.dataTable', this.rowSelector, null, function(e) {
            $this.onRowClick(e, this);
            $this.mousedownOnRow = false;
        });

        //double click
        if(this.hasBehavior('rowDblselect')) {
            this.tbody.off('dblclick.dataTable', this.rowSelector).on('dblclick.dataTable', this.rowSelector, null, function(e) {
                $this.onRowDblclick(e, $(this));
            });
        };

        this.bindSelectionKeyEvents();
    },

    /**
     * Sets up all delegated event listeners on the table body.
     * @private
     */
    bindSelectionKeyEvents: function() {
        var $this = this;

        this.getFocusableTbody().on('focus', function(e) {
            //ignore mouse click on row
            if(!$this.mousedownOnRow) {
                $this.focusedRow = $this.tbody.children('tr.ui-widget-content.ui-datatable-selectable.ui-state-highlight').eq(0);
                if ($this.focusedRow.length == 0) {
                    $this.focusedRow = $this.tbody.children('tr.ui-widget-content.ui-datatable-selectable').eq(0);
                }

                $this.highlightFocusedRow();

                if($this.cfg.scrollable) {
                    PrimeFaces.scrollInView($this.scrollBody, $this.focusedRow);
                }
            }
        })
        .on('blur', function() {
            if($this.focusedRow) {
                $this.unhighlightFocusedRow();
                $this.focusedRow = null;
            }
        })
        .on('keydown', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if($(e.target).is(':input')) {
                return;
            }

            if($this.focusedRow) {
                switch(key) {
                    case keyCode.UP:
                    case keyCode.DOWN:
                        var rowSelector = 'tr.ui-widget-content.ui-datatable-selectable',
                        row = key === keyCode.UP ? $this.focusedRow.prevAll(rowSelector).eq(0) : $this.focusedRow.nextAll(rowSelector).eq(0);

                        if(row.length) {
                            $this.unhighlightFocusedRow();

                            if($this.isCheckboxSelectionEnabled()) {
                                row.find('> td.ui-selection-column .ui-chkbox input').trigger('focus');
                            }
                            else {
                                $this.focusedRow = row;
                            }

                            $this.highlightFocusedRow();

                            if($this.cfg.scrollable) {
                                PrimeFaces.scrollInView($this.scrollBody, $this.focusedRow);
                            }
                        }
                        e.preventDefault();
                    break;

                    case keyCode.ENTER:
                    case keyCode.SPACE:
                        if($this.focusedRowWithCheckbox) {
                            $this.focusedRow.find('> td.ui-selection-column > div.ui-chkbox > div.ui-chkbox-box').trigger('click.dataTable');
                        }
                        else {
                            e.target = $this.focusedRow.children().eq(0).get(0);
                            $this.onRowClick(e,$this.focusedRow.get(0));
                        }

                        e.preventDefault();
                    break;

                    default:
                    break;
                };
            }
        });

    },

    /**
     * Highlights the currently focused row (if any) by adding the appropriate CSS class.
     * @protected
     */
    highlightFocusedRow: function() {
        this.focusedRow.addClass('ui-state-hover');
    },

    /**
     * Unhighlights the currently focused row (if any) by adding the appropriate CSS class.
     * @protected
     */
    unhighlightFocusedRow: function() {
        this.focusedRow.removeClass('ui-state-hover');
    },

    /**
     * Stores the row which is currently focused.
     * @protected
     * @param {JQuery} row Row to set as the focused row.
     */
    assignFocusedRow: function(row) {
        this.focusedRow = row;
    },

    /**
     * Sets up the event listeners for hovering over a data table row.
     * @protected
     * @param {string} selector Selector for the row elements. Any hover event that does not reach an element that
     * matches this selector will be ignored.
     */
    bindRowHover: function(selector) {
        this.tbody.off('mouseenter.dataTable mouseleave.dataTable', selector)
                    .on('mouseenter.dataTable', selector, null, function() {
                        $(this).addClass('ui-state-hover');
                    })
                    .on('mouseleave.dataTable', selector, null, function() {
                        $(this).removeClass('ui-state-hover');
                    });
    },

    /**
     * Sets up the event listeners for radio buttons contained in this data table.
     * @protected
     */
    bindRadioEvents: function() {
        var $this = this,
        radioInputSelector = '> tr.ui-widget-content:not(.ui-datatable-empty-message) > td.ui-selection-column :radio';

        if(this.cfg.nativeElements) {
            this.tbody.off('click.dataTable', radioInputSelector).on('click.dataTable', radioInputSelector, null, function(e) {
                var radioButton = $(this);

                if(!radioButton.prop('checked'))
                    $this.selectRowWithRadio(radioButton);
            });
        }
        else {
            var radioSelector = '> tr.ui-widget-content:not(.ui-datatable-empty-message) > td.ui-selection-column .ui-radiobutton .ui-radiobutton-box';
            this.tbody.off('click.dataTable mouseenter.dataTable mouseleave.dataTable', radioSelector)
                .on('mouseenter.dataTable', radioSelector, null, function() {
                    var radio = $(this);
                    if(!radio.hasClass('ui-state-disabled')) {
                        radio.addClass('ui-state-hover');
                    }
                })
                .on('mouseleave.dataTable', radioSelector, null, function() {
                    var radio = $(this);
                    radio.removeClass('ui-state-hover');
                })
                .on('click.dataTable', radioSelector, null, function() {
                    var radio = $(this),
                    checked = radio.hasClass('ui-state-active'),
                    disabled = radio.hasClass('ui-state-disabled');

                    if (!disabled) {
                        radio.prev().children(':radio').trigger('focus.dataTable');
                        if (!checked) {
                            $this.selectRowWithRadio(radio);
                        }
                    }
                });
        }

        //keyboard support
        this.tbody.off('focus.dataTable blur.dataTable change.dataTable', radioInputSelector)
            .on('focus.dataTable', radioInputSelector, null, function() {
                var input = $(this),
                box = input.parent().next();

                box.addClass('ui-state-focus');
            })
            .on('blur.dataTable', radioInputSelector, null, function() {
                var input = $(this),
                box = input.parent().next();

                box.removeClass('ui-state-focus');
            })
            .on('change.dataTable', radioInputSelector, null, function() {
                var currentInput = $this.tbody.find(radioInputSelector).filter(':checked'),
                currentRadio = currentInput.parent().next();

                $this.selectRowWithRadio(currentRadio);
            });

    },

    /**
     * Sets up the event listeners for radio buttons contained in this data table.
     * @protected
     */
    bindCheckboxEvents: function() {
        var $this = this,
        checkboxSelector;

        if(this.cfg.nativeElements) {
            checkboxSelector = '> tr.ui-widget-content.ui-datatable-selectable > td.ui-selection-column :checkbox';
            this.checkAllToggler = this.thead.find('> tr > th.ui-selection-column > :checkbox');

            this.checkAllToggler.on('click', function() {
                $this.toggleCheckAll();
            });

            this.tbody.off('click.dataTable', checkboxSelector).on('click.dataTable', checkboxSelector, null, function(e) {
                var checkbox = $(this);

                if(checkbox.prop('checked'))
                    $this.selectRowWithCheckbox(checkbox);
                else
                    $this.unselectRowWithCheckbox(checkbox);
            });
        }
        else {
            checkboxSelector = '> tr.ui-widget-content.ui-datatable-selectable > td.ui-selection-column > div.ui-chkbox > div.ui-chkbox-box';
            this.checkAllToggler = this.thead.find('> tr > th.ui-selection-column > div.ui-chkbox.ui-chkbox-all > div.ui-chkbox-box');

            this.checkAllToggler.on('mouseenter', function() {
                var box = $(this);
                if(!box.hasClass('ui-state-disabled')) {
                    box.addClass('ui-state-hover');
                }
            })
            .on('mouseleave', function() {
                $(this).removeClass('ui-state-hover');
            })
            .on('click', function() {
                var box = $(this);
                if(!box.hasClass('ui-state-disabled')) {
                    $this.toggleCheckAll();
                }
            })
            .on('keydown', function(e) {
                var keyCode = $.ui.keyCode,
                key = e.which;

                switch(key) {
                    case keyCode.ENTER:
                    case keyCode.SPACE:
                        if(!$(this).hasClass('ui-state-disabled')) {
                            $this.toggleCheckAll();
                        }
                    break;
                    default:
                    break;
                }
            });

            this.tbody.off('mouseenter.dataTable mouseleave.dataTable click.dataTable', checkboxSelector)
                        .on('mouseenter.dataTable', checkboxSelector, null, function() {
                            $(this).addClass('ui-state-hover');
                        })
                        .on('mouseleave.dataTable', checkboxSelector, null, function() {
                            $(this).removeClass('ui-state-hover');
                        })
                        .on('click.dataTable', checkboxSelector, null, function() {
                            var checkbox = $(this);

                            if(checkbox.attr('aria-checked') === "true") {
                                $this.unselectRowWithCheckbox(checkbox);
                            }
                            else {
                                $this.selectRowWithCheckbox(checkbox);
                            }
                        });
        }

        //keyboard support
        this.tbody.off('focus.dataTable blur.dataTable change.dataTable', checkboxSelector)
                    .on('focus.dataTable', checkboxSelector, null, function() {
                        var input = $(this);

                        input.addClass('ui-state-focus');

                        $this.focusedRow = input.closest('.ui-datatable-selectable');
                        $this.focusedRowWithCheckbox = true;
                    })
                    .on('blur.dataTable', checkboxSelector, null, function() {
                        var input = $(this);

                        input.removeClass('ui-state-focus');

                        $this.unhighlightFocusedRow();
                        $this.focusedRow = null;
                        $this.focusedRowWithCheckbox = false;
                    })
                    .on('change.dataTable', checkboxSelector, null, function(e) {
                        var input = $(this);

                        if(input.attr('aria-checked') === "true" || input.prop('checked')) {
                            $this.selectRowWithCheckbox(input);
                        }
                        else {
                            $this.unselectRowWithCheckbox(input);
                        }
                    });

        this.checkAllToggler.on('focus.dataTable', function(e) {
                        var input = $(this);

                        if(!input.hasClass('ui-state-disabled')) {
                            input.addClass('ui-state-focus');
                        }
                    })
                    .on('blur.dataTable', function(e) {
                        var input = $(this);

                        input.removeClass('ui-state-focus');
                    })
                    .on('change.dataTable', function(e) {
                        var input = $(this);

                        if(!input.hasClass('ui-state-disabled')) {
                            if((input.attr('aria-checked') !== "true") && !input.prop('checked')) {
                                input.addClass('ui-state-active');
                            }

                            $this.toggleCheckAll();

                            if(input.attr('aria-checked') === "true" || input.prop('checked')) {
                                input.removeClass('ui-state-active');
                            }
                        }
                    });
    },

    /**
     * Expands or collapses the given row, depending on whether it is currently collapsed or expanded, respectively.
     * @param {JQuery} row A row (`TR`) to expand or collapse.
     */
    toggleRow: function(row) {
        if(row && !this.isRowTogglerClicked) {
            var toggler = row.find('> td > div.ui-row-toggler');
            this.toggleExpansion(toggler);
        }
        this.isRowTogglerClicked = false;
    },

    /**
     * Applies events related to row expansion in a non-obstrusive way
     * @protected
     */
    bindExpansionEvents: function() {
        var $this = this,
        togglerSelector = '> tr > td > div.ui-row-toggler';

        this.tbody.off('click.datatable-expansion', togglerSelector)
            .on('click.datatable-expansion', togglerSelector, null, function() {
                $this.isRowTogglerClicked = true;
                $this.toggleExpansion($(this));
            })
            .on('keydown.datatable-expansion', togglerSelector, null, function(e) {
                var key = e.which,
                keyCode = $.ui.keyCode;

                if((key === keyCode.ENTER)) {
                    $this.toggleExpansion($(this));
                    e.preventDefault();
                }
            });
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.widget.ContextMenu} menuWidget
     * @param {PrimeFaces.widget.DataTable} targetWidget
     * @param {string} targetId
     * @param {PrimeFaces.widget.ContextMenuCfg} cfg
     */
    bindContextMenu : function(menuWidget, targetWidget, targetId, cfg) {
        var $this = this;
        var targetSelector = targetId + ' tbody.ui-datatable-data > tr.ui-widget-content';
        var targetEvent = cfg.event + '.datatable';
        this.contextMenuWidget = menuWidget;

        $(document).off(targetEvent, targetSelector).on(targetEvent, targetSelector, null, function(e) {
            var row = $(this);

            if(targetWidget.cfg.selectionMode && row.hasClass('ui-datatable-selectable')) {
                targetWidget.onRowRightClick(e, this, cfg.selectionMode);
                targetWidget.updateContextMenuCell(e, targetWidget);
                menuWidget.show(e);
            }
            else if(targetWidget.cfg.editMode === 'cell') {
                targetWidget.updateContextMenuCell(e, targetWidget);
                menuWidget.show(e);
            }
            else if(row.hasClass('ui-datatable-empty-message') && !$this.cfg.disableContextMenuIfEmpty) {
                menuWidget.show(e);
            }
        });

        if(this.cfg.scrollable && this.scrollBody) {
            this.scrollBody.off('scroll.dataTable-contextmenu').on('scroll.dataTable-contextmenu', function() {
                if($this.contextMenuWidget.jq.is(':visible')) {
                    $this.contextMenuWidget.hide();
                }
            });
        }
    },

    /**
     * Updates the currently selected cell based on where the context menu right click occurred.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that occurred.
     * @param {PrimeFaces.widget.DataTable} targetWidget The current widget
     */
    updateContextMenuCell: function(event, targetWidget) {
        var target = $(event.target),
        cell = target.is('td.ui-editable-column') ? target : target.parents('td.ui-editable-column:first');

        if(targetWidget.contextMenuCell) {
            targetWidget.contextMenuCell.removeClass('ui-state-highlight');
        }

        targetWidget.contextMenuClick = true;
        targetWidget.contextMenuCell = cell;
        targetWidget.contextMenuCell.addClass('ui-state-highlight');
    },

    /**
     * Sets up the event listeners for clicking on a row.
     * @private
     */
    bindRowClick: function() {
        var $this = this,
        rowSelector = '> tr.ui-widget-content:not(.ui-expanded-row-content)';
        this.tbody.off('click.dataTable-rowclick', rowSelector).on('click.dataTable-rowclick', rowSelector, null, function(e) {
            var target = $(e.target),
            row = target.is('tr.ui-widget-content') ? target : target.closest('tr.ui-widget-content');

            $this.cfg.onRowClick.call(this, row);
        });
    },

    /**
     * Reflow mode is a responsive mode to display columns as stacked depending on screen size.
     * @private
     */
    initReflow: function() {
        var headerColumns = this.thead.find('> tr > th');

        for(var i = 0; i < headerColumns.length; i++) {
            var headerColumn = headerColumns.eq(i),
            reflowHeaderText = headerColumn.find('.ui-reflow-headertext:first').text(),
            colTitleEl = headerColumn.children('.ui-column-title'),
            title = (reflowHeaderText && reflowHeaderText.length) ? reflowHeaderText : colTitleEl.text();
            this.tbody.find('> tr:not(.ui-datatable-empty-message,.ui-datatable-summaryrow) > td:nth-child(' + (i + 1) + ')').prepend('<span class="ui-column-title">' + PrimeFaces.escapeHTML(title) + '</span>');
        }
    },

    /**
     * Prepares this data table for the current scrolling settings and sets up all related event handlers.
     * @protected
     */
    setupScrolling: function() {
        this.scrollHeader = this.jq.children('.ui-datatable-scrollable-header');
        this.scrollBody = this.jq.children('.ui-datatable-scrollable-body');
        this.scrollFooter = this.jq.children('.ui-datatable-scrollable-footer');
        this.scrollStateHolder = $(this.jqId + '_scrollState');
        this.scrollHeaderBox = this.scrollHeader.children('div.ui-datatable-scrollable-header-box');
        this.scrollFooterBox = this.scrollFooter.children('div.ui-datatable-scrollable-footer-box');
        this.headerTable = this.scrollHeaderBox.children('table');
        this.bodyTable = this.cfg.virtualScroll ? this.scrollBody.children('div').children('table') : this.scrollBody.children('table');
        this.footerTable = this.scrollFooter.children('table');
        this.footerCols = this.scrollFooter.find('> .ui-datatable-scrollable-footer-box > table > tfoot > tr > td');
        this.percentageScrollHeight = this.cfg.scrollHeight && (this.cfg.scrollHeight.indexOf('%') !== -1);
        this.percentageScrollWidth = this.cfg.scrollWidth && (this.cfg.scrollWidth.indexOf('%') !== -1);
        var $this = this,
        scrollBarWidth = this.getScrollbarWidth() + 'px',
        hScrollWidth = this.scrollBody[0].scrollWidth;

        if(this.cfg.scrollHeight) {
            if(this.percentageScrollHeight) {
                this.adjustScrollHeight();
            }

            if(this.hasVerticalOverflow()) {
                this.scrollHeaderBox.css('margin-right', scrollBarWidth);
                this.scrollFooterBox.css('margin-right', scrollBarWidth);
            }
        }

        this.fixColumnWidths();

        if(this.cfg.scrollWidth) {
            if(this.percentageScrollWidth)
                this.adjustScrollWidth();
            else
                this.setScrollWidth(parseInt(this.cfg.scrollWidth));
        }

        this.cloneHead();

        if(this.cfg.liveScroll) {
            this.clearScrollState();
            this.scrollOffset = 0;
            this.cfg.liveScrollBuffer = (100 - this.cfg.liveScrollBuffer) / 100;
            this.shouldLiveScroll = true;
            this.loadingLiveScroll = false;
            this.allLoadedLiveScroll = $this.cfg.scrollStep >= $this.cfg.scrollLimit;
        }

        this.restoreScrollState();

        if(this.cfg.virtualScroll) {
            var row = this.bodyTable.children('tbody').children('tr.ui-widget-content');
            if(row) {
                var hasEmptyMessage = row.eq(0).hasClass('ui-datatable-empty-message'),
                scrollLimit = $this.cfg.scrollLimit;

                if(hasEmptyMessage) {
                    scrollLimit = 1;
                    $this.bodyTable.css('top', '0px');
                }

                this.rowHeight = row.outerHeight();
                this.scrollBody.children('div').css('height', parseFloat((scrollLimit * this.rowHeight + 1) + 'px'));

                if(hasEmptyMessage && this.cfg.scrollHeight && this.percentageScrollHeight) {
                    setTimeout(function() {
                        $this.adjustScrollHeight();
                    }, 10);
                }
            }
        }

        this.scrollBody.on('scroll.dataTable', function() {
            var scrollLeft = $this.scrollBody.scrollLeft();

            if ($this.isRTL) {
                $this.scrollHeaderBox.css('margin-right', (scrollLeft - hScrollWidth + this.clientWidth) + 'px');
                $this.scrollFooterBox.css('margin-right', (scrollLeft - hScrollWidth + this.clientWidth) + 'px');
            }
            else {
                $this.scrollHeaderBox.css('margin-left', -scrollLeft + 'px');
                $this.scrollFooterBox.css('margin-left', -scrollLeft + 'px');
            }

            if($this.isEmpty()) {
                return;
            }

            if($this.cfg.virtualScroll) {
                var virtualScrollBody = this;

                clearTimeout($this.scrollTimeout);
                $this.scrollTimeout = setTimeout(function() {
                    var viewportHeight = $this.scrollBody.outerHeight(),
                    tableHeight = $this.bodyTable.outerHeight(),
                    pageHeight = $this.rowHeight * $this.cfg.scrollStep,
                    virtualTableHeight = parseFloat(($this.cfg.scrollLimit * $this.rowHeight) + 'px'),
                    pageCount = (virtualTableHeight / pageHeight)||1;

                    if(virtualScrollBody.scrollTop + viewportHeight > parseFloat($this.bodyTable.css('top')) + tableHeight || virtualScrollBody.scrollTop < parseFloat($this.bodyTable.css('top'))) {
                        var page = Math.floor((virtualScrollBody.scrollTop * pageCount) / (virtualScrollBody.scrollHeight)) + 1;
                        $this.loadRowsWithVirtualScroll(page, function () {
                            $this.bodyTable.css('top', ((page - 1) * pageHeight) + 'px');
                        });
                    }
                }, 200);
            }
            else if($this.shouldLiveScroll) {
                var scrollTop = Math.ceil(this.scrollTop),
                scrollHeight = this.scrollHeight,
                viewportHeight = this.clientHeight;

                if((scrollTop >= ((scrollHeight * $this.cfg.liveScrollBuffer) - (viewportHeight))) && $this.shouldLoadLiveScroll()) {
                    $this.loadLiveRows();
                }
            }

            $this.saveScrollState();
        });

        this.scrollHeader.on('scroll.dataTable', function() {
            $this.scrollHeader.scrollLeft(0);
        });

        this.scrollFooter.on('scroll.dataTable', function() {
            $this.scrollFooter.scrollLeft(0);
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.jq, function() {
            if ($this.percentageScrollHeight) {
                $this.adjustScrollHeight();
            }
            if ($this.percentageScrollWidth) {
                $this.adjustScrollWidth();
            }
        });
    },

    /**
     * When live scrolling (loading more items on-demand) is enabled, checks whether more items are allowed to be loaded
     * right now. Returns `false` when live scroling is disabled or items are currently being loaded already.
     * @private
     * @return {boolean} `true` if more items may be loaded, `false` otherwise.
     */
    shouldLoadLiveScroll: function() {
        return (!this.loadingLiveScroll && !this.allLoadedLiveScroll);
    },

    /**
     * Clones a table header and removes duplicate IDs.
     * @private
     * @param {JQuery} thead The head (`THEAD`) of the table to clone.
     * @param {JQuery} table The table to which the head belongs.
     * @return {JQuery} The cloned table head.
     */
    cloneTableHeader: function(thead, table) {
        var clone = thead.clone();
        clone.find('th').each(function() {
            var header = $(this);
            header.attr('id', header.attr('id') + '_clone');
            $(this).children().not('.ui-column-title').remove();
            $(this).children('.ui-column-title').children().remove();
        });
        clone.removeAttr('id').addClass('ui-datatable-scrollable-theadclone').height(0).prependTo(table);

        return clone;
    },

    /**
     * Creates and stores a cloned copy of the table head(er) of this data table, and sets up some event handers.
     * @protected
     */
    cloneHead: function() {
        var $this = this;

        if (this.theadClone) {
            this.theadClone.remove();
        }
        this.theadClone = this.cloneTableHeader(this.thead, this.bodyTable);

        //reflect events from clone to original
        if(this.cfg.sorting) {
            this.sortableColumns.removeAttr('tabindex').off('blur.dataTable focus.dataTable keydown.dataTable');

            var clonedColumns = this.theadClone.find('> tr > th'),
            clonedSortableColumns = clonedColumns.filter('.ui-sortable-column');
            clonedColumns.each(function() {
                var col = $(this),
                originalId = col.attr('id').split('_clone')[0];
                if(col.hasClass('ui-sortable-column')) {
                    col.data('original', originalId);
                }

                $(PrimeFaces.escapeClientId(originalId))[0].style.width = col[0].style.width;
            });

            clonedSortableColumns.on('blur.dataTable', function() {
                $(PrimeFaces.escapeClientId($(this).data('original'))).removeClass('ui-state-focus');
            })
            .on('focus.dataTable', function() {
                $(PrimeFaces.escapeClientId($(this).data('original'))).addClass('ui-state-focus');
            })
            .on('keydown.dataTable', function(e) {
                var key = e.which,
                keyCode = $.ui.keyCode;

                if((key === keyCode.ENTER) && $(e.target).is(':not(:input)')) {
                    $(PrimeFaces.escapeClientId($(this).data('original'))).trigger('click.dataTable', (e.metaKey||e.ctrlKey));
                    e.preventDefault();
                }
            });
        }
    },

    /**
     * Adjusts the height of the body of this data table for the current scrolling settings.
     * @protected
     */
    adjustScrollHeight: function() {
        var relativeHeight = this.jq.parent().innerHeight() * (parseInt(this.cfg.scrollHeight) / 100),
        headerChilden = this.jq.children('.ui-datatable-header'),
        footerChilden = this.jq.children('.ui-datatable-footer'),
        tableHeaderHeight = (headerChilden.length > 0) ? headerChilden.outerHeight(true) : 0,
        tableFooterHeight = (footerChilden.length > 0) ? footerChilden.outerHeight(true) : 0,
        scrollersHeight = (this.scrollHeader.outerHeight(true) + this.scrollFooter.outerHeight(true)),
        paginatorsHeight = this.paginator ? this.paginator.getContainerHeight(true) : 0,
        height = (relativeHeight - (scrollersHeight + paginatorsHeight + tableHeaderHeight + tableFooterHeight));

        if(this.cfg.virtualScroll) {
            this.scrollBody.css('max-height', height + 'px');
        }
        else {
            this.scrollBody.height(height);
        }
    },

    /**
     * Adjusts the width of the header, body, and footer of this data table to fit the current settings.
     * @protected
     */
    adjustScrollWidth: function() {
        var width = parseInt((this.jq.parent().innerWidth() * (parseInt(this.cfg.scrollWidth) / 100)));
        this.setScrollWidth(width);
    },

    /**
     * Applies the given width to this data table.
     * @private
     * @param {JQuery} element Element of the data table.
     * @param {number} width New width in pixels to set.
     */
    setOuterWidth: function(element, width) {
        if (element.css('box-sizing') === 'border-box') { // Github issue: #5014
            element.outerWidth(width);
        }
        else {
            element.width(width);
        }
    },

    /**
     * Retrieves width information of the given column.
     * @private
     * @param {JQuery} col The column of which the width should be retrieved.
     * @param {boolean} isIncludeResizeableState Tells whether the width should be retrieved from the resizable state,
     * if it exists.
     * @return {PrimeFaces.widget.DataTable.WidthInfo} The width information of the given column.
     */
    getColumnWidthInfo: function(col, isIncludeResizeableState) {
        var $this = this;
        var width, isOuterWidth;

        if(isIncludeResizeableState && this.resizableState) {
            width = $this.findColWidthInResizableState(col.attr('id'));
            isOuterWidth = false;
        }

        if(!width) {
            width = col[0].style.width;
            isOuterWidth = width && (col.css('box-sizing') === 'border-box');
        }

        if(!width) {
            width = col.width();
            isOuterWidth = false;
        }

        return {
            width: width,
            isOuterWidth: isOuterWidth
        };
    },

    /**
     * Applies the width information to the given element.
     * @private
     * @param {JQuery} element The element to which the width should be applied.
     * @param {PrimeFaces.widget.DataTable.WidthInfo} widthInfo The width information (retrieved using the method {@link getColumnWidthInfo}).
     */
    applyWidthInfo: function(element, widthInfo) {
        if(widthInfo.isOuterWidth) {
            element.outerWidth(widthInfo.width);
        }
        else {
            element.width(widthInfo.width);
        }
    },

    /**
     * Applies the given scroll width to this data table.
     * @protected
     * @param {number} width Scroll width in pixels to set.
     */
    setScrollWidth: function(width) {
        var $this = this;
        this.jq.children('.ui-widget-header').each(function() {
            $this.setOuterWidth($(this), width);
        });
        this.scrollHeader.width(width);
        this.scrollBody.css('margin-right', '0px').width(width);
        this.scrollFooter.width(width);
    },

    /**
     * Adds some margin to the scroll body to make it align properly.
     * @private
     */
    alignScrollBody: function() {
        var marginRight = this.hasVerticalOverflow() ? this.getScrollbarWidth() + 'px' : '0px';

        this.scrollHeaderBox.css('margin-right', marginRight);
        this.scrollFooterBox.css('margin-right', marginRight);
    },

    /**
     * Finds the width of the current scrollbar used for this data table.
     * @private
     * @return {number} The width in pixels of the scrollbar of this data table.
     */
    getScrollbarWidth: function() {
        if(!this.scrollbarWidth) {
            this.scrollbarWidth = PrimeFaces.env.browser.webkit ? '15' : PrimeFaces.calculateScrollbarWidth();
        }

        return this.scrollbarWidth;
    },

    /**
     * Checks whether the body of this data table overflow vertically.
     * @protected
     * @return {boolean} `true` if any content overflow vertically, `false` otherwise.
     */
    hasVerticalOverflow: function() {
        return (this.cfg.scrollHeight && this.bodyTable.outerHeight() > this.scrollBody.outerHeight());
    },

    /**
     * Reads the saved scroll state and applies it. This helps to preserve the current scrolling position during AJAX
     * updates.
     * @private
     */
    restoreScrollState: function() {
        var scrollState = this.scrollStateHolder.val(),
        scrollValues = scrollState.split(',');

        if (scrollValues[0] == '-1') {
            scrollValues[0] = this.scrollBody[0].scrollWidth;
        }

        this.scrollBody.scrollLeft(scrollValues[0]);
        this.scrollBody.scrollTop(scrollValues[1]);
    },

    /**
     * Saves the current scrolling position. This helps to preserve the current scrolling position during AJAX updates.
     * @private
     */
    saveScrollState: function() {
        var scrollState = this.scrollBody.scrollLeft() + ',' + this.scrollBody.scrollTop();

        this.scrollStateHolder.val(scrollState);
    },

    /**
     * Clears the saved scrolling position.
     * @private
     */
    clearScrollState: function() {
        this.scrollStateHolder.val('0,0');
    },

    /**
     * Adjusts the width of the given columns to fit the current settings.
     * @protected
     */
    fixColumnWidths: function() {
        var $this = this;

        if(!this.columnWidthsFixed) {
            if(this.cfg.scrollable) {
                this.scrollHeader.find('> .ui-datatable-scrollable-header-box > table > thead > tr > th').each(function() {
                    var headerCol = $(this),
                    colIndex = headerCol.index(),
                    widthInfo = $this.getColumnWidthInfo(headerCol, true);

                    $this.applyWidthInfo(headerCol, widthInfo);

                    if($this.footerCols.length > 0) {
                        var footerCol = $this.footerCols.eq(colIndex);
                        $this.applyWidthInfo(footerCol, widthInfo);
                    }
                });
            }
            else {
                var columns = this.jq.find('> .ui-datatable-tablewrapper > table > thead > tr > th'),
                    visibleColumns = columns.filter(':visible'),
                    hiddenColumns = columns.filter(':hidden');

                this.setColumnsWidth(visibleColumns);
                /* IE fixes */
                this.setColumnsWidth(hiddenColumns);
            }

            this.columnWidthsFixed = true;
        }
    },

    /**
     * Applies the appropriated width to all given column elements.
     * @param {JQuery} columns A list of column elements.
     * @private
     */
    setColumnsWidth: function(columns) {
        if(columns.length) {
            var $this = this;

            columns.each(function() {
                var col = $(this),
                widthInfo = $this.getColumnWidthInfo(col, true);

                $this.applyWidthInfo(col, widthInfo);
            });
        }
    },

    /**
     * Use only when live scrolling is enabled: Loads the next set of rows on-the-fly.
     */
    loadLiveRows: function() {
        if(this.liveScrollActive||(this.scrollOffset + this.cfg.scrollStep > this.cfg.scrollLimit)) {
            return;
        }

        this.liveScrollActive = true;
        this.scrollOffset += this.cfg.scrollStep;

        //Disable scroll if there is no more data left
        if(this.scrollOffset === this.cfg.scrollLimit) {
            this.shouldLiveScroll = false;
        }

        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_scrolling', value: true},
                            {name: this.id + '_first', value: 1},
                            {name: this.id + '_skipChildren', value: true},
                            {name: this.id + '_scrollOffset', value: this.scrollOffset},
                            {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        //insert new rows
                        this.updateData(content, false);

                        this.liveScrollActive = false;
                    }
                });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(typeof args.totalRecords !== 'undefined') {
                    $this.cfg.scrollLimit = args.totalRecords;
                }

                $this.loadingLiveScroll = false;
                $this.allLoadedLiveScroll = ($this.scrollOffset + $this.cfg.scrollStep) >= $this.cfg.scrollLimit;

                // reset index of shift selection on multiple mode
                $this.originRowIndex = null;
            }
        };

        if (this.hasBehavior('liveScroll')) {
            this.callBehavior('liveScroll', options);
        } else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When live scrolling is enabled: Loads the next set of rows via AJAX.
     * @private
     * @param {number} page 0-based index of the page to load.
     * @param {() => void} callback Callback that is invoked after the rows have been loaded and inserted into the DOM.
     */
    loadRowsWithVirtualScroll: function(page, callback) {
        if(this.virtualScrollActive) {
            return;
        }

        this.virtualScrollActive = true;

        var $this = this,
        first = (page - 1) * this.cfg.scrollStep,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_scrolling', value: true},
                            {name: this.id + '_skipChildren', value: true},
                            {name: this.id + '_first', value: first},
                            {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        //insert new rows
                        this.updateData(content);
                        callback();
                        this.virtualScrollActive = false;
                    }
                });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(typeof args.totalRecords !== 'undefined') {
                    $this.cfg.scrollLimit = args.totalRecords;
                }

                // reset index of shift selection on multiple mode
                $this.originRowIndex = null;
            }
        };
        if (this.hasBehavior('virtualScroll')) {
            this.callBehavior('virtualScroll', options);
        } else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Switches to the given page by loading the content via AJAX. Compare with `loadDataWithCache`, which first checks
     * whether the data is already cached and loads it from the server only when not found in the cache.
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new values for the current page and the rows
     * per page count.
     */
    paginate: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_pagination', value: true},
                    {name: this.id + '_first', value: newState.first},
                    {name: this.id + '_rows', value: newState.rows},
                    {name: this.id + '_skipChildren', value: true},
                    {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.updateData(content);

                            if(this.checkAllToggler) {
                                this.updateHeaderCheckbox();
                            }

                            if(this.cfg.scrollable) {
                                this.alignScrollBody();
                            }

                            if(this.cfg.clientCache) {
                                this.cacheMap[newState.first] = content;
                            }
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                $this.paginator.cfg.page = newState.page;
                if(args && typeof args.totalRecords !== 'undefined') {
                    $this.paginator.updateTotalRecords(args.totalRecords);
                }
                else {
                    $this.paginator.updateUI();
                }
                $this.updateColumnsView();
                // reset index of shift selection on multiple mode
                $this.originRowIndex = null;
            }
        };

        if(this.hasBehavior('page')) {
            this.callBehavior('page', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Loads next page asynchronously to keep it at viewstate and Updates viewstate
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new values for the current page and the rows
     * per page count.
     */
    fetchNextPage: function(newState) {
        var rows = newState.rows,
        first = newState.first,
        $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_skipChildren', value: true},
                    {name: this.id + '_encodeFeature', value: true},
                    {name: this.id + '_first', value: first},
                    {name: this.id + '_rows', value: rows},
                    {name: this.id + '_pagination', value: true},
                    {name: this.id + '_clientCache', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        if(content.length) {
                            var nextFirstValue = first + rows;
                            $this.cacheMap[nextFirstValue] = content;
                        }
                    }
                });

                return true;
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Updates and syncs the current pagination state with the server.
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new values for the current page and the rows
     * per page count.
     * @private
     */
    updatePageState: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_pagination', value: true},
                    {name: this.id + '_encodeFeature', value: true},
                    {name: this.id + '_pageState', value: true},
                    {name: this.id + '_first', value: newState.first},
                    {name: this.id + '_rows', value: newState.rows}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        // do nothing
                    }
                });

                return true;
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Performs a sorting operation on the rows of this data table via AJAX
     * @param {JQuery} columnHeader Header of the column by which to sort.
     * @param {-1 | 0 | 1} order Whether to "unsort" or to sort by the column value in an ascending or descending order.
     * @param {boolean} multi `true` if sorting by multiple columns is enabled, or `false` otherwise.
     * @private
     */
    sort: function(columnHeader, order, multi) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_sorting', value: true},
                     {name: this.id + '_skipChildren', value: true},
                     {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.updateData(content);

                            if(this.checkAllToggler) {
                              this.updateHeaderCheckbox();
                            }
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                var paginator = $this.getPaginator();
                if(args) {
                    if(args.totalRecords) {
                        $this.cfg.scrollLimit = args.totalRecords;

                        if(paginator && paginator.cfg.rowCount !== args.totalRecords) {
                            paginator.setTotalRecords(args.totalRecords);
                        }
                    }

                    if(!args.validationFailed) {
                        if(paginator) {
                            paginator.setPage(0, true);
                        }

                        // remove aria-sort
                        var activeColumns = $this.sortableColumns.filter('.ui-state-active');
                        if(activeColumns.length) {
                            activeColumns.removeAttr('aria-sort');
                        }
                        else {
                            $this.sortableColumns.eq(0).removeAttr('aria-sort');
                        }

                        if(!multi) {
                            //aria reset
                            for(var i = 0; i < activeColumns.length; i++) {
                                var activeColumn = $(activeColumns.get(i)),
                                    ariaLabelOfActive = activeColumn.attr('aria-label');

                                activeColumn.attr('aria-label', $this.getSortMessage(ariaLabelOfActive, $this.ascMessage));
                                $(PrimeFaces.escapeClientId(activeColumn.attr('id') + '_clone')).removeAttr('aria-sort').attr('aria-label', $this.getSortMessage(ariaLabelOfActive, $this.ascMessage));
                            }

                            activeColumns.data('sortorder', $this.SORT_ORDER.UNSORTED).removeClass('ui-state-active')
                                        .find('.ui-sortable-column-icon').removeClass('ui-icon-triangle-1-n ui-icon-triangle-1-s');
                        }

                        columnHeader.data('sortorder', order).addClass('ui-state-active');
                        var sortIcon = columnHeader.find('.ui-sortable-column-icon'),
                        ariaLabel = columnHeader.attr('aria-label');

                        if (order === $this.SORT_ORDER.DESCENDING) {
                            sortIcon.removeClass('ui-icon-triangle-1-n').addClass('ui-icon-triangle-1-s');
                            columnHeader.attr('aria-sort', 'descending').attr('aria-label', $this.getSortMessage(ariaLabel, $this.otherMessage));
                            $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'descending')
                                .attr('aria-label', $this.getSortMessage(ariaLabel, $this.otherMessage));
                        } else if (order === $this.SORT_ORDER.ASCENDING) {
                            sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-n');
                            columnHeader.attr('aria-sort', 'ascending').attr('aria-label', $this.getSortMessage(ariaLabel, $this.descMessage));
                            $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'ascending')
                                .attr('aria-label', $this.getSortMessage(ariaLabel, $this.descMessage));
                        } else {
                            sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-carat-2-n-s');
                            columnHeader.removeClass('ui-state-active ').attr('aria-sort', 'other')
                                .attr('aria-label', $this.getSortMessage(ariaLabel, $this.ascMessage));
                            $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'other')
                                .attr('aria-label', $this.getSortMessage(ariaLabel, $this.ascMessage));
                        }

                        $this.updateSortPriorityIndicators();
                    }
                }

                if($this.cfg.virtualScroll) {
                    $this.resetVirtualScrollBody();
                }
                else if($this.cfg.liveScroll) {
                    $this.scrollOffset = 0;
                    $this.liveScrollActive = false;
                    $this.shouldLiveScroll = true;
                    $this.loadingLiveScroll = false;
                    $this.allLoadedLiveScroll = $this.cfg.scrollStep >= $this.cfg.scrollLimit;
                }

                if($this.cfg.clientCache) {
                    $this.clearCacheMap();
                }

                $this.updateColumnsView();

                // reset index of shift selection on multiple mode
                $this.originRowIndex = null;
            }
        };

        options.params.push({name: this.id + '_sortKey', value: $this.joinSortMetaOption('col')});
        options.params.push({name: this.id + '_sortDir', value: $this.joinSortMetaOption('order')});

        if(this.hasBehavior('sort')) {
            this.callBehavior('sort', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },
    
    /**
     * In multi-sort mode this will add number indicators to let the user know the current 
     * sort order. If only one column is sorted then no indicator is displayed and will
     * only be displayed once more than one column is sorted.
     * @private
     */
    updateSortPriorityIndicators: function() {
        var $this = this;

        // remove all indicator numbers first
        $this.sortableColumns.find('.ui-sortable-column-badge').text('').addClass('ui-helper-hidden');

        // add 1,2,3 etc to columns if more than 1 column is sorted
        var sortMeta =  $this.sortMeta;
        if (sortMeta && sortMeta.length > 1) {
            $this.sortableColumns.each(function() {
                var id = $(this).attr("id");
                for (var i = 0; i < sortMeta.length; i++) {
                    if (sortMeta[i].col == id) {
                        $(this).find('.ui-sortable-column-badge').text(i + 1).removeClass('ui-helper-hidden');
                    }
                }
            });
        }
    },

    /**
     * Serializes the option from the sort meta items.
     * @private
     * @param {keyof PrimeFaces.widget.DataTable.SortMeta} option Property of the sort meta to use.
     * @return {string} All values from the current sort meta list for the given option.
     */
    joinSortMetaOption: function(option) {
        var value = '';

        for(var i = 0; i < this.sortMeta.length; i++) {
            value += this.sortMeta[i][option];

            if(i !== (this.sortMeta.length - 1)) {
                value += ',';
            }
        }

        return value;
    },

    /**
     * Filters this data table. Uses the current values of the filter inputs. This will result in an AJAX request being
     * sent.
     */
    filter: function() {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_filtering', value: true},
                     {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.updateData(content);

                            if(this.cfg.scrollable) {
                                this.alignScrollBody();
                            }

                            if(this.isCheckboxSelectionEnabled()) {
                                this.updateHeaderCheckbox();
                            }
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                var paginator = $this.getPaginator();
                if(args && typeof args.totalRecords !== 'undefined') {
                    $this.cfg.scrollLimit = args.totalRecords;

                    if(paginator) {
                        paginator.setTotalRecords(args.totalRecords);
                    }
                }

                if($this.cfg.clientCache) {
                    $this.clearCacheMap();
                }

                if($this.cfg.virtualScroll) {
                    var row = $this.bodyTable.children('tbody').children('tr.ui-widget-content');
                    if(row) {
                        var hasEmptyMessage = row.eq(0).hasClass('ui-datatable-empty-message'),
                        scrollLimit = $this.cfg.scrollLimit;

                        if(hasEmptyMessage) {
                            scrollLimit = 1;
                        }

                        $this.resetVirtualScrollBody();

                        $this.rowHeight = row.outerHeight();
                        $this.scrollBody.children('div').css({'height': parseFloat((scrollLimit * $this.rowHeight + 1) + 'px')});

                        if(hasEmptyMessage && $this.cfg.scrollHeight && $this.percentageScrollHeight) {
                            setTimeout(function() {
                                $this.adjustScrollHeight();
                            }, 10);
                        }
                    }
                }
                else if($this.cfg.liveScroll) {
                    $this.scrollOffset = 0;
                    $this.liveScrollActive = false;
                    $this.shouldLiveScroll = true;
                    $this.loadingLiveScroll = false;
                    $this.allLoadedLiveScroll = $this.cfg.scrollStep >= $this.cfg.scrollLimit;
                }

                $this.updateColumnsView();
                $this.updateEmptyColspan();

                // reset index of shift selection on multiple mode
                $this.originRowIndex = null;
            }
        };

        if(this.hasBehavior('filter')) {
            this.callBehavior('filter', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Callback for a click event on a row.
     * @private
     * @param {JQuery.TriggeredEvent} event Click event that occurred.
     * @param {HTMLElement} rowElement Row that was clicked
     * @param {boolean} silent `true` to prevent behaviors from being invoked, `false` otherwise.
     */
    onRowClick: function(event, rowElement, silent) {
        //Check if rowclick triggered this event not a clickable element in row content
        if($(event.target).is(this.rowSelectorForRowClick)) {
            var row = $(rowElement),
            selected = row.hasClass('ui-state-highlight'),
            metaKey = event.metaKey||event.ctrlKey,
            shiftKey = event.shiftKey;

            this.assignFocusedRow(row);

            //unselect a selected row if metakey is on
            if(selected && metaKey) {
                this.unselectRow(row, silent);
            }
            else {
                //unselect previous selection if this is single selection or multiple one with no keys
                if(this.isSingleSelection() || (this.isMultipleSelection() && event && !metaKey && !shiftKey && this.cfg.rowSelectMode === 'new' )) {
                    this.unselectAllRows();
                }

                //range selection with shift key
                if(this.isMultipleSelection() && event && event.shiftKey && this.originRowIndex !== null) {
                    this.selectRowsInRange(row);
                }
                else if(this.cfg.rowSelectMode === 'add' && selected) {
                    this.unselectRow(row, silent);
                }
                //select current row
                else {
                    this.originRowIndex = row.index();
                    this.cursorIndex = null;
                    this.selectRow(row, silent);
                }
            }

            if(this.cfg.disabledTextSelection) {
                PrimeFaces.clearSelection();
            }

            //#3567 trigger client row click on ENTER/SPACE
            if (this.cfg.onRowClick && event.type === "keydown") {
                this.cfg.onRowClick.call(this, row);
            }
        }
    },

    /**
     * Callback for a double click event on a row.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that occurred.
     * @param {JQuery} row Row that was clicked.
     */
    onRowDblclick: function(event, row) {
        if(this.cfg.disabledTextSelection) {
            PrimeFaces.clearSelection();
        }

        //Check if rowclick triggered this event not a clickable element in row content
        if($(event.target).is('td,span:not(.ui-c)')) {
            var rowMeta = this.getRowMeta(row);

            this.fireRowSelectEvent(rowMeta.key, 'rowDblselect');
        }
    },

    /**
     * Callback for a right click event on a row. May bring up the context menu
     * @private
     * @param {JQuery.TriggeredEvent} event Event that occurred.
     * @param {JQuery} rowElement Row that was clicked.
     * @param {PrimeFaces.widget.DataTable.CmSelectionMode} cmSelMode The current selection mode.
     */
    onRowRightClick: function(event, rowElement, cmSelMode) {
        var row = $(rowElement),
        rowMeta = this.getRowMeta(row),
        selected = row.hasClass('ui-state-highlight');

        this.assignFocusedRow(row);

        if(cmSelMode === 'single' || !selected) {
            this.unselectAllRows();
        }

        this.selectRow(row, true);

        this.fireRowSelectEvent(rowMeta.key, 'contextMenu');

        if(this.cfg.disabledTextSelection) {
            PrimeFaces.clearSelection();
        }
    },

    /**
     * Converts a row specifier to the row element. The row specifier is either a row index or the row element itself.
     *
     * __In case this data table has got expandable rows, please not that a new table row is created for each expanded row.__
     * This may result in the given index not pointing to the intended row.
     * @param {PrimeFaces.widget.DataTable.RowSpecifier} r The row to convert.
     * @return {JQuery} The row, or an empty JQuery instance of no row was found.
     */
    findRow: function(r) {
        var row = r;

        if(PrimeFaces.isNumber(r)) {
            row = this.tbody.children('tr:eq(' + r + ')');
        }

        return row;
    },

    /**
     * Select the rows between the cursor and the given row.
     * @private
     * @param {JQuery} row A row of this data table.
     */
    selectRowsInRange: function(row) {
        var rows = this.tbody.children(),
        rowMeta = this.getRowMeta(row),
        $this = this;

        //unselect previously selected rows with shift
        if(this.cursorIndex !== null) {
            var oldCursorIndex = this.cursorIndex,
            rowsToUnselect = oldCursorIndex > this.originRowIndex ? rows.slice(this.originRowIndex, oldCursorIndex + 1) : rows.slice(oldCursorIndex, this.originRowIndex + 1);

            rowsToUnselect.each(function(i, item) {
                $this.unselectRow($(item), true);
            });
        }

        //select rows between cursor and origin
        this.cursorIndex = row.index();

        var rowsToSelect = this.cursorIndex > this.originRowIndex ? rows.slice(this.originRowIndex, this.cursorIndex + 1) : rows.slice(this.cursorIndex, this.originRowIndex + 1);

        rowsToSelect.each(function(i, item) {
            $this.selectRow($(item), true);
        });

        this.fireRowSelectEvent(rowMeta.key, 'rowSelect');
    },

    /**
     * Selects the given row, according to the current selection mode.
     * @param {PrimeFaces.widget.DataTable.RowSpecifier} r A row of this data table to select.
     * @param {boolean} [silent] `true` to prevent behaviors and event listeners from being invoked, or `false`
     * otherwise.
     */
    selectRow: function(r, silent) {
        var row = this.findRow(r);
        if(!row.hasClass('ui-datatable-selectable')) {
            return;
        }

        // #5944 in single select all other rows should be unselected
        if (this.isSingleSelection() || this.isRadioSelectionEnabled()) {
            this.unselectAllRows();
        }

        var rowMeta = this.getRowMeta(row);

        this.highlightRow(row);

        if(this.isCheckboxSelectionEnabled()) {
            if(this.cfg.nativeElements)
                row.children('td.ui-selection-column').find(':checkbox').prop('checked', true);
            else
                this.selectCheckbox(row.children('td.ui-selection-column').find('> div.ui-chkbox > div.ui-chkbox-box'));

            this.updateHeaderCheckbox();
        }

        if(this.isRadioSelectionEnabled()) {
            if(this.cfg.nativeElements)
                row.children('td.ui-selection-column').find(':radio').prop('checked', true);
            else
                this.selectRadio(row.children('td.ui-selection-column').find('> div.ui-radiobutton > div.ui-radiobutton-box'));
        }

        this.addSelection(rowMeta.key);

        this.writeSelections();

        if(!silent) {
            this.fireRowSelectEvent(rowMeta.key, 'rowSelect');
        }
    },

    /**
     * Unselects the given row.
     * @param {PrimeFaces.widget.DataTable.RowSpecifier} r A row of this data table to unselect.
     * @param {boolean} [silent] `true` to prevent behaviors and event listeners from being invoked, or `false`
     * otherwise.
     */
    unselectRow: function(r, silent) {
        var row = this.findRow(r);
        if(!row.hasClass('ui-datatable-selectable')) {
            return;
        }

        var rowMeta = this.getRowMeta(row);

        this.unhighlightRow(row);

        if(this.isCheckboxSelectionEnabled()) {
            if(this.cfg.nativeElements)
                row.children('td.ui-selection-column').find(':checkbox').prop('checked', false);
            else
                this.unselectCheckbox(row.children('td.ui-selection-column').find('> div.ui-chkbox > div.ui-chkbox-box'));

            this.updateHeaderCheckbox();
        }

        this.removeSelection(rowMeta.key);

        this.writeSelections();

        if(!silent) {
            this.fireRowUnselectEvent(rowMeta.key, "rowUnselect");
        }
    },

    /**
     * Highlights row to mark it as selected.
     * @protected
     * @param {JQuery} row Row to highlight.
     */
    highlightRow: function(row) {
        row.addClass('ui-state-highlight').attr('aria-selected', true);
    },

    /**
     * Unhighlights row so it is no longer marked as selected.
     * @protected
     * @param {JQuery} row Row to unhighlight.
     */
    unhighlightRow: function(row) {
        row.removeClass('ui-state-highlight').attr('aria-selected', false);
    },

    /**
     * Sends a row select event on server side to invoke a row select listener if defined.
     * @private
     * @param {string} rowKey The key of the row that was selected.
     * @param {string} behaviorEvent Name of the event to fire.
     */
    fireRowSelectEvent: function(rowKey, behaviorEvent) {
        if(this.hasBehavior(behaviorEvent)) {
            var ext = {
                    params: [{name: this.id + '_instantSelectedRowKey', value: rowKey}
                ]
            };

            this.callBehavior(behaviorEvent, ext);
        }
    },

    /**
     * Sends a row unselect event on server side to invoke a row unselect listener if defined
     * @private
     * @param {string} rowKey The key of the row that was deselected.
     * @param {string} behaviorEvent Name of the event to fire.
     */
    fireRowUnselectEvent: function(rowKey, behaviorEvent) {
        if(this.hasBehavior(behaviorEvent)) {
            var ext = {
                params: [
                {
                    name: this.id + '_instantUnselectedRowKey',
                    value: rowKey
                }
                ]
            };

            this.callBehavior(behaviorEvent, ext);
        }
    },

    /**
     * Selects the corresponding row of a radio based column selection
     * @private
     * @param {JQuery} radio A radio INPUT element
     */
    selectRowWithRadio: function(radio) {
        var row = radio.closest('tr'),
        rowMeta = this.getRowMeta(row);

        //clean selection
        this.unselectAllRows();

        //select current
        if(!this.cfg.nativeElements) {
            this.selectRadio(radio);
        }

        this.highlightRow(row);
        this.addSelection(rowMeta.key);
        this.writeSelections();
        this.fireRowSelectEvent(rowMeta.key, 'rowSelectRadio');
    },

    /**
     * Selects the corresponding row of a checkbox based column selection
     * @private
     * @param {JQuery} checkbox A checkox INPUT element
     * @param {boolean} [silent] `true` to prevent behaviors from being invoked, `false` otherwise.
     */
    selectRowWithCheckbox: function(checkbox, silent) {
        var row = checkbox.closest('tr');
        if(!row.hasClass('ui-datatable-selectable')) {
            return;
        }

        var rowMeta = this.getRowMeta(row);

        this.highlightRow(row);

        if(!this.cfg.nativeElements) {
            this.selectCheckbox(checkbox);
        }

        this.addSelection(rowMeta.key);

        this.writeSelections();

        if(!silent) {
            this.updateHeaderCheckbox();
            this.fireRowSelectEvent(rowMeta.key, "rowSelectCheckbox");
        }
    },

    /**
     * Unselects the corresponding row of a checkbox based column selection
     * @private
     * @param {JQuery} checkbox A checkox INPUT element
     * @param {boolean} [silent] `true` to prevent behaviors from being invoked, `false` otherwise.
     */
    unselectRowWithCheckbox: function(checkbox, silent) {
        var row = checkbox.closest('tr');
        if(!row.hasClass('ui-datatable-selectable')) {
            return;
        }

        var rowMeta = this.getRowMeta(row);

        this.unhighlightRow(row);

        if(!this.cfg.nativeElements) {
            this.unselectCheckbox(checkbox);
        }

        this.removeSelection(rowMeta.key);

        this.uncheckHeaderCheckbox();

        this.writeSelections();

        if(!silent) {
            this.fireRowUnselectEvent(rowMeta.key, "rowUnselectCheckbox");
        }
    },

    /**
     * Unselects all rows of this data table so that no rows are selected. This includes all rows on all pages,
     * irrespective of whether they are on the currently shown page.
     */
    unselectAllRows: function() {
        var selectedRows = this.tbody.children('tr.ui-state-highlight'),
        checkboxSelectionEnabled = this.isCheckboxSelectionEnabled(),
        radioSelectionEnabled = this.isRadioSelectionEnabled();

        for(var i = 0; i < selectedRows.length; i++) {
            var row = selectedRows.eq(i);
            if(!row.hasClass('ui-datatable-selectable')) {
                continue;
            }

            this.unhighlightRow(row);

            if(checkboxSelectionEnabled) {
                if(this.cfg.nativeElements)
                    row.children('td.ui-selection-column').find(':checkbox').prop('checked', false);
                else
                    this.unselectCheckbox(row.children('td.ui-selection-column').find('> div.ui-chkbox > div.ui-chkbox-box'));
            }
            else if(radioSelectionEnabled) {
                if(this.cfg.nativeElements)
                    row.children('td.ui-selection-column').find(':radio').prop('checked', false);
                else
                    this.unselectRadio(row.children('td.ui-selection-column').find('> div.ui-radiobutton > div.ui-radiobutton-box'));
            }
        }

        if(checkboxSelectionEnabled) {
            this.uncheckHeaderCheckbox();
        }

        this.selection = [];
        this.writeSelections();
    },

    /**
     * Select all rows on the currently shown page. Compare with `selectAllRows`.
     */
    selectAllRowsOnPage: function() {
        var rows = this.tbody.children('tr');
        for(var i = 0; i < rows.length; i++) {
            var row = rows.eq(i);
            this.selectRow(row, true);
        }
    },

    /**
     * Unselect all rows on the currently shown page. Compare with `unselectAllRows`.
     */
    unselectAllRowsOnPage: function() {
        var rows = this.tbody.children('tr');
        for(var i = 0; i < rows.length; i++) {
            var row = rows.eq(i);
            this.unselectRow(row, true);
        }
    },

     /**
     * Selects all rows of this data table so that no rows are selected. This includes all rows on all pages,
     * irrespective of whether they are on the currently shown page.
     */
    selectAllRows: function() {
        this.selectAllRowsOnPage();
        this.selection = new Array('@all');
        this.writeSelections();
    },

    /**
     * Toggles the `selected all` checkbox in the header of this data table. When no rows are selected, this will select
     * all rows. When some rows are selected, this will unselect all rows.
     */
    toggleCheckAll: function() {
        var shouldCheckAll = true;
        if(this.cfg.nativeElements) {
            var checkboxes = this.tbody.find('> tr.ui-datatable-selectable > td.ui-selection-column > :checkbox:visible'),
            checked = this.checkAllToggler.prop('checked'),
            $this = this;

            checkboxes.each(function() {
                if(checked) {
                    var checkbox = $(this);
                    checkbox.prop('checked', true);
                    $this.selectRowWithCheckbox(checkbox, true);
                }
                else {
                    var checkbox = $(this);
                    checkbox.prop('checked', false);
                    $this.unselectRowWithCheckbox(checkbox, true);
                    shouldCheckAll = false;
                }
            });
        }
        else {
            var checkboxes = this.tbody.find('> tr.ui-datatable-selectable > td.ui-selection-column > div.ui-chkbox > div.ui-chkbox-box:visible'),
            checked = this.checkAllToggler.attr('aria-checked') === "true";
            $this = this;

            if(checked) {
                this.checkAllToggler.removeClass('ui-state-active').children('span.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
                this.checkAllToggler.attr('aria-checked', false);
                shouldCheckAll = false;

                checkboxes.each(function() {
                    $this.unselectRowWithCheckbox($(this), true);
                });
            }
            else {
                this.checkAllToggler.addClass('ui-state-active').children('span.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
                this.checkAllToggler.attr('aria-checked', true);

                checkboxes.each(function() {
                    $this.selectRowWithCheckbox($(this), true);
                });
            }
        }

        // GitHub #6730 user wants all rows not just displayed rows
        if(!this.cfg.selectionPageOnly && shouldCheckAll) {
            this.selectAllRows();
        }

        //save state
        this.writeSelections();

        //fire toggleSelect event
        if(this.hasBehavior('toggleSelect')) {
            var ext = {
                params: [{name: this.id + '_checked', value: !checked}]
            };

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

    /**
     * Selects the given checkbox from a row.
     * @private
     * @param {JQuery} checkbox A checkbox to select.
     */
    selectCheckbox: function(checkbox) {
        checkbox.addClass('ui-state-active');

        if (this.cfg.nativeElements) {
            checkbox.prop('checked', true);
        }
        else {
            checkbox.children('span.ui-chkbox-icon:first').removeClass('ui-icon-blank').addClass('ui-icon-check');
            checkbox.attr('aria-checked', true);
        }
    },

    /**
     * Unselects the given checkbox from a row.
     * @private
     * @param {JQuery} checkbox A checkbox to unselect.
     */
    unselectCheckbox: function(checkbox) {
        checkbox.removeClass('ui-state-active');

        if (this.cfg.nativeElements) {
            checkbox.prop('checked', false);
        }
        else {
            checkbox.children('span.ui-chkbox-icon:first').addClass('ui-icon-blank').removeClass('ui-icon-check');
            checkbox.attr('aria-checked', false);
        }
    },

    /**
     * Selects the given radio button from a row.
     * @private
     * @param {JQuery} radio A radio button to select.
     */
    selectRadio: function(radio){
        radio.addClass('ui-state-active');
        radio.children('.ui-radiobutton-icon').addClass('ui-icon-bullet').removeClass('ui-icon-blank');
        radio.prev().children('input').prop('checked', true);
    },

    /**
     * Unselects the given radio button from a row.
     * @private
     * @param {JQuery} radio A radio button to unselect.
     */
    unselectRadio: function(radio){
        radio.removeClass('ui-state-active').children('.ui-radiobutton-icon').addClass('ui-icon-blank').removeClass('ui-icon-bullet');
        radio.prev().children('input').prop('checked', false);
    },

    /**
     * Expands a row to display its detailed content
     * @private
     * @param {JQuery} toggler The row toggler of a row to expand.
     */
    toggleExpansion: function(toggler) {
        var row = toggler.closest('tr'),
        rowIndex = this.getRowMeta(row).index,
        iconOnly = toggler.hasClass('ui-icon'),
        labels = toggler.children('span'),
        expanded = iconOnly ? toggler.hasClass('ui-icon-circle-triangle-s'): toggler.children('span').eq(0).hasClass('ui-helper-hidden'),
        $this = this;

        //Run toggle expansion if row is not being toggled already to prevent conflicts
        if($.inArray(rowIndex, this.expansionProcess) === -1) {
            this.expansionProcess.push(rowIndex);

            if(expanded) {
                if(iconOnly) {
                    toggler.addClass('ui-icon-circle-triangle-e').removeClass('ui-icon-circle-triangle-s').attr('aria-expanded', false);
                }
                else {
                    labels.eq(0).removeClass('ui-helper-hidden');
                    labels.eq(1).addClass('ui-helper-hidden');
                }

                this.collapseRow(row);
                $this.expansionProcess = $.grep($this.expansionProcess, function(r) {
                    return (r !== rowIndex);
                });
                this.fireRowCollapseEvent(row);
            }
            else {
                if(this.cfg.rowExpandMode === 'single') {
                    this.collapseAllRows();
                }

                if(iconOnly) {
                    toggler.addClass('ui-icon-circle-triangle-s').removeClass('ui-icon-circle-triangle-e').attr('aria-expanded', true);
                }
                else {
                    labels.eq(0).addClass('ui-helper-hidden');
                    labels.eq(1).removeClass('ui-helper-hidden');
                }

                this.loadExpandedRowContent(row);
            }
        }
    },

    /**
     * Loads the detailed content for the given expandable row.
     * @private
     * @param {JQuery} row A row with content to load.
     */
    loadExpandedRowContent: function(row) {
        // To check whether or not any hidden expansion content exists to avoid reloading multiple duplicate nodes in DOM
        var expansionContent = row.next('.ui-expanded-row-content');
        if(expansionContent.length > 0) {
            expansionContent.remove();
        }

        var $this = this,
        rowIndex = this.getRowMeta(row).index,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_rowExpansion', value: true},
                     {name: this.id + '_expandedRowIndex', value: rowIndex},
                     {name: this.id + '_encodeFeature', value: true},
                     {name: this.id + '_skipChildren', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            if(content && PrimeFaces.trim(content).length) {
                                row.addClass('ui-expanded-row');
                                this.rowExpansionLoaded(rowIndex);
                                this.displayExpandedRow(row, content);
                            }
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.expansionProcess = $.grep($this.expansionProcess, function(r) {
                    return r !== rowIndex;
                });
            }
        };

        if(this.hasBehavior('rowToggle')) {
            this.callBehavior('rowToggle', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Display the given HTML string in the specified row. Called mainly after an AJAX request.
     * @protected
     * @param {JQuery} row Row to display.
     * @param {string} content HTML string of the content to add to the row
     */
    displayExpandedRow: function(row, content) {
        row.after(content);
        this.updateColspan(row.next());
    },

    /**
     * Calls the behaviors and event listeners when a row is collapsed.
     * @private
     * @param {JQuery} row A row of this data table.
     */
    fireRowCollapseEvent: function(row) {
        var rowIndex = this.getRowMeta(row).index;

        if(this.hasBehavior('rowToggle')) {
            var ext = {
                params: [
                    {name: this.id + '_collapsedRowIndex', value: rowIndex},
                    {name: this.id + '_skipChildren', value: true}
                ]
            };
            this.callBehavior('rowToggle', ext);
        }
    },

    /**
     * Collapses the given row, if it is expandable. Use `findRow` to get a row by its index. Does not update the row
     * expansion toggler button.
     * @protected
     * @param {JQuery} row Row to collapse.
     */
    collapseRow: function(row) {
        // #942: need to use "hide" instead of "remove" to avoid incorrect form mapping when a row is collapsed
        row.removeClass('ui-expanded-row').next('.ui-expanded-row-content').hide();
    },

    /**
     * Collapses all rows that are currently expanded.
     */
    collapseAllRows: function() {
        var $this = this;

        this.getExpandedRows().each(function() {
            var expandedRow = $(this);
            $this.collapseRow(expandedRow);

            var columns = expandedRow.children('td');
            for(var i = 0; i < columns.length; i++) {
                var column = columns.eq(i),
                toggler = column.children('.ui-row-toggler');

                if(toggler.length > 0) {
                    if(toggler.hasClass('ui-icon')) {
                        toggler.addClass('ui-icon-circle-triangle-e').removeClass('ui-icon-circle-triangle-s');
                    }
                    else {
                        var labels = toggler.children('span');
                        labels.eq(0).removeClass('ui-helper-hidden');
                        labels.eq(1).addClass('ui-helper-hidden');
                    }
                    break;
                }
            }
        });
    },

    /**
     * Finds the list of row that are currently expanded.
     * @return {JQuery} All rows (`TR`) that are currently expanded.
     */
    getExpandedRows: function() {
        return this.tbody.children('.ui-expanded-row');
    },

    /**
     * Binds editor events non-obstrusively
     * @private
     */
    bindEditEvents: function() {
        var $this = this;
        this.cfg.saveOnCellBlur = (this.cfg.saveOnCellBlur === false) ? false : true;

        if(this.cfg.editMode === 'row') {
            var rowEditorSelector = '> tr > td > div.ui-row-editor > a';

            this.tbody.off('click.datatable focus.datatable blur.datatable', rowEditorSelector)
                        .on('click.datatable', rowEditorSelector, null, function(e) {
                            var element = $(this),
                            row = element.closest('tr');

                            if(element.hasClass('ui-row-editor-pencil')) {
                                $this.switchToRowEdit(row);
                                element.hide().siblings().show();
                            }
                            else if(element.hasClass('ui-row-editor-check')) {
                                $this.saveRowEdit(row);
                            }
                            else if(element.hasClass('ui-row-editor-close')) {
                                $this.cancelRowEdit(row);
                            }

                            e.preventDefault();
                        })
                        .on('focus.datatable', rowEditorSelector, null, function(e) {
                            $(this).addClass('ui-row-editor-outline');
                        })
                        .on('blur.datatable', rowEditorSelector, null, function(e) {
                            $(this).removeClass('ui-row-editor-outline');
                        });

            // GitHub #433 Allow ENTER to submit ESC to cancel row editor
            $(document).off("keydown.datatable", "tr.ui-row-editing")
                        .on("keydown.datatable", "tr.ui-row-editing", function(e) {
                            var keyCode = $.ui.keyCode;
                            switch (e.which) {
                                case keyCode.ENTER:
                                    var target = $(e.target);
                                    // GitHub #7028
                                    if(target.is("textarea")) {
                                         return true;
                                    }
                                    $(this).closest("tr").find(".ui-row-editor-check").trigger("click");
                                    return false; // prevents executing other event handlers (adding new row to the table)
                                case keyCode.ESCAPE:
                                    $(this).closest("tr").find(".ui-row-editor-close").trigger("click");
                                    return false;
                                default:
                                    break;
                }
            });
        }
        else if(this.cfg.editMode === 'cell') {
            var originalCellSelector = '> tr > td.ui-editable-column'
            cellSelector = this.cfg.cellSeparator || originalCellSelector,
            editEvent = (this.cfg.editInitEvent !== 'click') ? this.cfg.editInitEvent + '.datatable-cell click.datatable-cell' : 'click.datatable-cell';

            if (this.cfg.cellSeparator) {
                this.tbody.off(editEvent, originalCellSelector)
                    .on(editEvent, originalCellSelector, null, function (e) {
                        $this.incellClick = true;

                        if (!$(this).hasClass('ui-cell-editing') && e.type === $this.cfg.editInitEvent && $this.cfg.editInitEvent === "dblclick") {
                            $this.incellClick = false;
                        }
                    });
            }

            this.tbody.off(editEvent, cellSelector)
                        .on(editEvent, cellSelector, null, function(e) {
                            $this.incellClick = true;

                            var item = $(this),
                            cell = item.hasClass('ui-editable-column') ? item : item.closest('.ui-editable-column');

                            if(!cell.hasClass('ui-cell-editing') && e.type === $this.cfg.editInitEvent) {
                                $this.showCellEditor(cell);

                                if($this.cfg.editInitEvent === "dblclick") {
                                    $this.incellClick = false;
                                }
                            }
                        });

            $(document).off('click.datatable-cell-blur' + this.id)
                        .on('click.datatable-cell-blur' + this.id, function(e) {
                            var target = $(e.target);
                            if(!$this.incellClick && (target.is('.ui-input-overlay') || target.closest('.ui-input-overlay').length || target.closest('.ui-datepicker-buttonpane').length)) {
                                $this.incellClick = true;
                            }

                            if(!$this.incellClick && $this.currentCell && !$this.contextMenuClick && !$.datepicker._datepickerShowing && $('.p-datepicker-panel:visible').length === 0) {
                                if($this.cfg.saveOnCellBlur)
                                    $this.saveCell($this.currentCell);
                                else
                                    $this.doCellEditCancelRequest($this.currentCell);
                            }

                            $this.incellClick = false;
                            $this.contextMenuClick = false;
                        });
        }
    },

    /**
     * Switch all editable columns of the given row to their editing mode, if editing is enabled on this data table.
     * Use `findRow` to get a row by its index.
     * @param {JQuery} row A row (`TR`) to switch to edit mode.
     */
    switchToRowEdit: function(row) {
        // #1499 disable rowReorder while editing
        if (this.cfg.draggableRows) {
            this.tbody.sortable("disable");
        }

        if(this.cfg.rowEditMode === "lazy") {
            this.lazyRowEditInit(row);
        }
        else {
            this.showRowEditors(row);

            if(this.hasBehavior('rowEditInit')) {
                var rowIndex = this.getRowMeta(row).index;

                var ext = {
                    params: [{name: this.id + '_rowEditIndex', value: rowIndex}]
                };

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

    /**
     * Shows the row editor(s) for the given row (and hides the normal output display).
     * @protected
     * @param {JQuery} row Row for which to show the row editor.
     */
    showRowEditors: function(row) {
        row.addClass('ui-state-highlight ui-row-editing').children('td.ui-editable-column').each(function() {
            var column = $(this);

            column.find('.ui-cell-editor-output').hide();
            column.find('.ui-cell-editor-input').show();
        });

        var inputs=row.find(':input:enabled');
        if (inputs.length > 0) {
            inputs.first().trigger('focus');
        }
    },

    /**
     * Finds the meta data for a given cell.
     * @param {JQuery} cell A cell for which to get the meta data.
     * @return {string} The meta data of the given cell or NULL if not found
     */
    getCellMeta: function(cell) {
        var rowMeta = this.getRowMeta(cell.closest('tr')),
            cellIndex = cell.index();

        if(this.cfg.scrollable && this.cfg.frozenColumns) {
            cellIndex = (this.scrollTbody.is(cell.closest('tbody'))) ? (cellIndex + $this.cfg.frozenColumns) : cellIndex;
        }

        if (rowMeta === undefined || rowMeta.index === undefined) {
            return null;
        }
        var cellInfo = rowMeta.index + ',' + cellIndex;
        if(rowMeta.key) {
            cellInfo = cellInfo + ',' + rowMeta.key;
        }

        return cellInfo;
    },

    /**
     * Initializes the given cell so that its content can be edited (when row editing is enabled)
     * @private
     * @param {JQuery} cell A cell of this data table to set up.
     */
    cellEditInit: function(cell) {
        var cellInfo = this.getCellMeta(cell),
        cellEditor = cell.children('.ui-cell-editor'),
        $this = this;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_encodeFeature', value: true},
                    {name: this.id + '_cellEditInit', value: true},
                    {name: this.id + '_cellInfo', value: cellInfo}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-input').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                cell.data('edit-events-bound', false);
                $this.showCurrentCell(cell);
            }
        };

        if(this.hasBehavior('cellEditInit')) {
            this.callBehavior('cellEditInit', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When cell editing is enabled, shows the cell editor for the given cell that lets the user edit the cell content.
     * @param {JQuery} c A cell (`TD`) of this data table to edit.
     */
    showCellEditor: function(c) {
        this.incellClick = true;

        var cell = null;

        if(c) {
            cell = c;

            //remove contextmenu selection highlight
            if(this.contextMenuCell) {
                this.contextMenuCell.parent().removeClass('ui-state-highlight');
            }
        }
        else {
            cell = this.contextMenuCell;
        }

        var editorInput = cell.find('> .ui-cell-editor > .ui-cell-editor-input');
        if(editorInput.length !== 0 && editorInput.children().length === 0 && this.cfg.editMode === 'cell') {
            // for lazy cellEditMode
            this.cellEditInit(cell);
        }
        else {
            this.showCurrentCell(cell);

            if(this.hasBehavior('cellEditInit')) {
                var cellInfo = this.getCellMeta(cell);
                if (cellInfo) {
                    var ext = {
                        params: [{name: this.id + '_cellInfo', value: cellInfo}]
                    };
                    this.callBehavior('cellEditInit', ext);
                }
            }
        }
    },

    /**
     * Shows the cell editors for the given cell.
     * @private
     * @param {JQuery} cell A cell of this data table.
     */
    showCurrentCell: function(cell) {
        var $this = this;

        if(this.currentCell) {
            if(this.cfg.saveOnCellBlur)
                this.saveCell(this.currentCell);
            else if(!this.currentCell.is(cell))
                this.doCellEditCancelRequest(this.currentCell);
        }

        if(cell && cell.length) {
            this.currentCell = cell;

            var cellEditor = cell.children('div.ui-cell-editor'),
            displayContainer = cellEditor.children('div.ui-cell-editor-output'),
            inputContainer = cellEditor.children('div.ui-cell-editor-input'),
            inputs = inputContainer.find(':input:enabled'),
            multi = inputs.length > 1;

            cell.addClass('ui-state-highlight ui-cell-editing');
            displayContainer.hide();
            inputContainer.show();
            var input = inputs.eq(0);
            input.trigger('focus');
            input.trigger('select');

            //metadata
            if(multi) {
                var oldValues = [];
                for(var i = 0; i < inputs.length; i++) {
                    var input = inputs.eq(i);

                    if(input.is(':checkbox')) {
                        oldValues.push(input.val() + "_" + input.is(':checked'));
                    }
                    else {
                        oldValues.push(input.val());
                    }
                }

                cell.data('multi-edit', true);
                cell.data('old-value', oldValues);
            }
            else {
                cell.data('multi-edit', false);
                cell.data('old-value', inputs.eq(0).val());
            }

            //bind events on demand
            if(!cell.data('edit-events-bound')) {
                cell.data('edit-events-bound', true);

                inputs.on('keydown.datatable-cell', function(e) {
                        var keyCode = $.ui.keyCode,
                        shiftKey = e.shiftKey,
                        key = e.which,
                        input = $(this);

                        if(key === keyCode.ENTER) {
                            // GitHub #7028
                            if(input.is("textarea")) {
                                return true;
                            }
                            $this.saveCell(cell);
                            $this.currentCell = null;

                            e.preventDefault();
                        }
                        else if(key === keyCode.TAB) {
                            if(multi) {
                                var focusIndex = shiftKey ? input.index() - 1 : input.index() + 1;

                                if(focusIndex < 0 || (focusIndex === inputs.length) || input.parent().hasClass('ui-inputnumber') || input.parent().hasClass('ui-helper-hidden-accessible')) {
                                    $this.tabCell(cell, !shiftKey);
                                } else {
                                    inputs.eq(focusIndex).trigger('focus');
                                }
                            }
                            else {
                                $this.tabCell(cell, !shiftKey);
                            }

                            e.preventDefault();
                        }
                        else if(key === keyCode.ESCAPE) {
                            $this.doCellEditCancelRequest(cell);
                            e.preventDefault();
                        }
                    })
                    .on('focus.datatable-cell click.datatable-cell', function(e) {
                        $this.currentCell = cell;
                    });
            }
        }
        else {
            this.currentCell = null;
        }
    },

    /**
     * Moves to the next or previous editable cell when the tab key was pressed.
     * @private
     * @param {JQuery} cell The currently focused cell
     * @param {boolean} forward `true` if tabbing forward, `false` otherwise.
     */
    tabCell: function(cell, forward) {
        var targetCell = forward ? cell.nextAll('td.ui-editable-column:first') : cell.prevAll('td.ui-editable-column:first');
        if(targetCell.length == 0) {
            var tabRow = forward ? cell.parent().next() : cell.parent().prev();
            targetCell = forward ? tabRow.children('td.ui-editable-column:first') : tabRow.children('td.ui-editable-column:last');
        }

        var cellEditor = targetCell.children('div.ui-cell-editor'),
        inputContainer = cellEditor.children('div.ui-cell-editor-input');

        if(inputContainer.length) {
            var inputs = inputContainer.find(':input'),
            disabledInputs = inputs.filter(':disabled');

            if(inputs.length === disabledInputs.length) {
                this.tabCell(targetCell, forward);
                return;
            }
        }

        this.showCellEditor(targetCell);
    },

    /**
     * After the user is done editing a cell, saves the content of the given cell and switches back to view mode.
     * @param {JQuery} cell A cell (`TD`) in edit mode.
     */
    saveCell: function(cell) {
        var inputs = cell.find('div.ui-cell-editor-input :input:enabled'),
        changed = false,
        valid = cell.data('valid'),
        $this = this;

        if(cell.data('multi-edit')) {
            var oldValues = cell.data('old-value');
            for(var i = 0; i < inputs.length; i++) {
                var input = inputs.eq(i),
                    inputVal = input.val(),
                    oldValue = oldValues[i];

                if(input.is(':checkbox') || input.is(':radio')) {
                    inputVal = inputVal + "_" + input.is(':checked');
                }

                if(inputVal != oldValue) {
                    changed = true;
                    break;
                }
            }
        }
        else {
            var input = inputs.eq(0),
                inputVal = input.val(),
                oldValue = cell.data('old-value');

            if(input.is(':checkbox') || input.is(':radio')) {
                inputVal = inputVal + "_" + input.is(':checked');
            }
            changed = (inputVal != oldValue);
        }

        if(changed || valid == false)
            $this.doCellEditRequest(cell);
        else
            $this.viewMode(cell);

        if(this.cfg.saveOnCellBlur) {
            this.currentCell = null;
        }
    },

    /**
     * Switches the given cell to its view mode (not editable).
     * @private
     * @param {JQuery} cell A cell of this data table.
     */
    viewMode: function(cell) {
        var cellEditor = cell.children('div.ui-cell-editor'),
        editableContainer = cellEditor.children('div.ui-cell-editor-input'),
        displayContainer = cellEditor.children('div.ui-cell-editor-output');

        cell.removeClass('ui-cell-editing ui-state-error ui-state-highlight');
        displayContainer.show();
        editableContainer.hide();
        cell.removeData('old-value').removeData('multi-edit');

        if(this.cfg.cellEditMode === "lazy") {
            editableContainer.children().remove();
        }
    },

    /**
     * When the users clicks on an editable cell, runs the AJAX request to show the inline editor for the given cell.
     * @private
     * @param {JQuery} cell The cell to switch to edit mode.
     */
    doCellEditRequest: function(cell) {
        var rowMeta = this.getRowMeta(cell.closest('tr')),
        cellEditor = cell.children('.ui-cell-editor'),
        cellEditorId = cellEditor.attr('id'),
        cellIndex = cell.index(),
        $this = this;

        if(this.cfg.scrollable && this.cfg.frozenColumns) {
            cellIndex = (this.scrollTbody.is(cell.closest('tbody'))) ? (cellIndex + $this.cfg.frozenColumns) : cellIndex;
        }

        var cellInfo = rowMeta.index + ',' + cellIndex;
        if(rowMeta.key) {
            cellInfo = cellInfo + ',' + rowMeta.key;
        }

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [{name: this.id + '_encodeFeature', value: true},
                     {name: this.id + '_cellInfo', value: cellInfo},
                     {name: cellEditorId, value: cellEditorId}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-output').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(args.validationFailed){
                    cell.data('valid', false);
                    cell.addClass('ui-state-error');
                }
                else{
                    cell.data('valid', true);
                    $this.viewMode(cell);
                }

                if($this.cfg.clientCache) {
                    $this.clearCacheMap();
                }
            }
        };

        if(this.hasBehavior('cellEdit')) {
            this.callBehavior('cellEdit', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When the user wants to discard the edits to a cell, performs the required AJAX request for that.
     * @private
     * @param {JQuery} cell The cell in edit mode with changes to discard.
     */
    doCellEditCancelRequest: function(cell) {
        var rowMeta = this.getRowMeta(cell.closest('tr')),
        cellEditor = cell.children('.ui-cell-editor'),
        cellIndex = cell.index(),
        $this = this;

        if(this.cfg.scrollable && this.cfg.frozenColumns) {
            cellIndex = (this.scrollTbody.is(cell.closest('tbody'))) ? (cellIndex + $this.cfg.frozenColumns) : cellIndex;
        }

        var cellInfo = rowMeta.index + ',' + cellIndex;
        if(rowMeta.key) {
            cellInfo = cellInfo + ',' + rowMeta.key;
        }

        this.currentCell = null;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [{name: this.id + '_encodeFeature', value: true},
                     {name: this.id + '_cellEditCancel', value: true},
                     {name: this.id + '_cellInfo', value: cellInfo}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-input').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                $this.viewMode(cell);
                cell.data('edit-events-bound', false);

                if($this.cfg.clientCache) {
                    $this.clearCacheMap();
                }
            }
        };

        if(this.hasBehavior('cellEditCancel')) {
            this.callBehavior('cellEditCancel', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When the given row is currently being edited, saves the contents of the edited row and switch back to view mode.
     * Use `findRow` to get a row by its index.
     * @param {JQuery} rowEditor A row (`TR`) in edit mode to save.
     */
    saveRowEdit: function(rowEditor) {
        this.doRowEditRequest(rowEditor, 'save');
    },

    /**
     * When the given row is currently being edited, cancel the editing operation and discard the entered data. Use
     * `findRow` to get a row by its index.
     * @param {JQuery} rowEditor A row (`TR`) in edit mode.
     */
    cancelRowEdit: function(rowEditor) {
        this.doRowEditRequest(rowEditor, 'cancel');
    },

    /**
     * Sends an AJAX request to handle row save or cancel
     * @private
     * @param {JQuery} rowEditor The curent row editor
     * @param {PrimeFaces.widget.DataTable.RowEditAction} action Whether to save or cancel the row edit.
     */
    doRowEditRequest: function(rowEditor, action) {
        var row = rowEditor.closest('tr'),
        rowIndex = this.getRowMeta(row).index,
        expanded = row.hasClass('ui-expanded-row'),
        $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_rowEditIndex', value: this.getRowMeta(row).index},
                     {name: this.id + '_rowEditAction', value: action},
                     {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            if(expanded) {
                                this.collapseRow(row);
                            }

                            this.updateRow(row, content);

                            // #1499 enable rowReorder when done editing
                            if (this.cfg.draggableRows && $('tr.ui-row-editing').length === 0) {
                                this.tbody.sortable("enable");
                            }

                            // #258 must reflow after editing
                            this.postUpdateData();
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(args && args.validationFailed) {
                    $this.invalidateRow(rowIndex);
                }
                else {
                    if($this.cfg.rowEditMode === "lazy") {
                        var index = ($this.paginator) ? (rowIndex % $this.paginator.getRows()) : rowIndex,
                        newRow = $this.tbody.children('tr').eq(index);
                        $this.getRowEditors(newRow).children('.ui-cell-editor-input').children().remove();
                    }
                }

                if($this.cfg.clientCache) {
                    $this.clearCacheMap();
                }
            }
        };

        if(action === 'save') {
            this.getRowEditors(row).each(function() {
                options.params.push({name: this.id, value: this.id});
            });
        }

        if(action === 'save' && this.hasBehavior('rowEdit')) {
            this.callBehavior('rowEdit', options);
        }
        else if(action === 'cancel' && this.hasBehavior('rowEditCancel')) {
            this.callBehavior('rowEditCancel', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Performs the required initialization for making a row editable. Only called on-demand when the row actually needs
     * to be edited.
     * @private
     * @param {JQuery} row A row of this data table.
     */
    lazyRowEditInit: function(row) {
        var rowIndex = this.getRowMeta(row).index,
        $this = this;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_encodeFeature', value: true},
                    {name: this.id + '_rowEditInit', value: true},
                    {name: this.id + '_rowEditIndex', value: rowIndex}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            $this.updateRow(row, content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                var index = ($this.paginator) ? (rowIndex % $this.paginator.getRows()) : rowIndex,
                newRow = $this.tbody.children('tr').eq(index);
                $this.showRowEditors(newRow);
            }
        };

        if(this.hasBehavior('rowEditInit')) {
            this.cfg.behaviors['rowEditInit'].call(this, options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Updates a row with the given content
     * @protected
     * @param {JQuery} row Row to update.
     * @param {string} content HTML string to set on the row.
     */
    updateRow: function(row, content) {
        row.replaceWith(content);
    },

    /**
     * Displays row editors in invalid format.
     * @protected
     * @param {number} index 0-based index of the row to invalidate.
     */
    invalidateRow: function(index) {
        var i = (this.paginator) ? (index % this.paginator.getRows()) : index;
        this.tbody.children('tr[data-ri]').eq(i).addClass('ui-widget-content ui-row-editing ui-state-error');
    },

    /**
     * Finds all editors of a row. Usually each editable column has got an editor.
     * @protected
     * @param {JQuery} row A row for which to find its row editors.
     * @return {JQuery} A list of row editors for each editable column of the given row
     */
    getRowEditors: function(row) {
        return row.find('div.ui-cell-editor');
    },

    /**
     * Returns the paginator instance if any exists.
     * @return {PrimeFaces.widget.Paginator | undefined} The paginator instance for this widget, or `undefined` if
     * paging is not enabled.
     */
    getPaginator: function() {
        return this.paginator;
    },

    /**
     * Writes selected row ids to state holder
     * @private
     */
    writeSelections: function() {
        $(this.selectionHolder).val(this.selection.join(','));
    },

    /**
     * Checks whether only one row may be selected at a time.
     * @return {boolean} `true` if selection mode is set to `single`, or `false` otherwise.
     */
    isSingleSelection: function() {
        return this.cfg.selectionMode == 'single';
    },

    /**
     * Checks whether multiples rows may be selected at a time.
     * @return {boolean} `true` if selection mode is set to `multiple`, or `false` otherwise.
     */
    isMultipleSelection: function() {
        return this.cfg.selectionMode == 'multiple' || this.isCheckboxSelectionEnabled();
    },

    /**
     * Clears the saved list of selected rows.
     * @private
     */
    clearSelection: function() {
        this.selection = [];

        $(this.selectionHolder).val('');
    },

    /**
     * Checks whether the user may select the rows of this data table.
     * @return {boolean} `true` is rows may be selected, or `false` otherwise.
     */
    isSelectionEnabled: function() {
        return this.cfg.selectionMode != undefined || this.cfg.columnSelectionMode != undefined;
    },

    /**
     * Checks whether the rows of this data table are selected via checkboxes.
     * @return {boolean} `true` if selection mode is set to `checkbox`, or `false` otherwise.
     */
    isCheckboxSelectionEnabled: function() {
        return this.cfg.selectionMode === 'checkbox';
    },

    /**
     * Checks whether the rows of this data table are selected via radio buttons.
     * @return {boolean} `true` if selection mode is set to `radio`, or `false` otherwise.
     */
    isRadioSelectionEnabled: function() {
        return this.cfg.selectionMode === 'radio';
    },

    /**
     * Clears all table filters and shows all rows that may have been hidden by filters.
     */
    clearFilters: function() {
        this.thead.find('> tr > th.ui-filter-column > .ui-column-filter').val('');
        this.thead.find('> tr > th.ui-filter-column > .ui-column-customfilter :input').val('');
        $(this.jqId + '\\:globalFilter').val('');

        this.filter();
    },

    /**
     * Sets up the event listeners to enable columns to be resized.
     * @private
     */
    setupResizableColumns: function() {
        this.cfg.resizeMode = this.cfg.resizeMode||'fit';

        this.fixColumnWidths();

        this.hasColumnGroup = this.hasColGroup();
        if(this.hasColumnGroup) {
            this.addGhostRow();
        }

        if(!this.cfg.liveResize) {
            this.resizerHelper = $('<div class="ui-column-resizer-helper ui-state-highlight"></div>').appendTo(this.jq);
        }

        this.addResizers();

        var resizers = this.thead.find('> tr > th > span.ui-column-resizer'),
        $this = this;

        resizers.draggable({
            axis: 'x',
            start: function(event, ui) {
                ui.helper.data('originalposition', ui.helper.offset());

                if($this.cfg.liveResize) {
                    $this.jq.css('cursor', 'col-resize');
                }
                else {
                    var header = $this.cfg.stickyHeader ? $this.clone : $this.thead,
                        height = $this.cfg.scrollable ? $this.scrollBody.height() : header.parent().height() - header.height() - 1;

                    if($this.cfg.stickyHeader) {
                        height = height - $this.relativeHeight;
                    }

                    $this.resizerHelper.height(height);
                    $this.resizerHelper.show();
                }
            },
            drag: function(event, ui) {
                if($this.cfg.liveResize) {
                    $this.resize(event, ui);
                }
                else {
                    $this.resizerHelper.offset({
                        left: ui.helper.offset().left + ui.helper.width() / 2,
                        top: $this.thead.offset().top + $this.thead.height()
                    });
                }
            },
            stop: function(event, ui) {
                ui.helper.css({
                    'left': '',
                    'top': '0px'
                });

                if($this.cfg.liveResize) {
                    $this.jq.css('cursor', 'default');
                } else {
                    $this.resize(event, ui);
                    $this.resizerHelper.hide();
                }

                if($this.cfg.resizeMode === 'expand') {
                    setTimeout(function() {
                        $this.fireColumnResizeEvent(ui.helper.parent());
                    }, 5);
                }
                else {
                    $this.fireColumnResizeEvent(ui.helper.parent());
                }

                if($this.cfg.stickyHeader) {
                    $this.reclone();
                }
            },
            containment: this.cfg.resizeMode === "expand" ? "document" : this.jq
        });
    },

    /**
     * Invokes the behaviors and event listeners when a column is resized.
     * @private
     * @param {JQuery} columnHeader Header of the column which was resized.
     */
    fireColumnResizeEvent: function(columnHeader) {
        if(this.hasBehavior('colResize')) {
            var options = {
                source: this.id,
                process: this.id,
                params: [
                    {name: this.id + '_colResize', value: true},
                    {name: this.id + '_columnId', value: columnHeader.attr('id')},
                    {name: this.id + '_width', value: parseInt(columnHeader.width())},
                    {name: this.id + '_height', value: parseInt(columnHeader.height())}
                ]
            };

            this.callBehavior('colResize', options);
        }
    },

    /**
     * Checks whether this data table has got any column groups.
     * @protected
     * @return {boolean} `true` if this data table has got any column groups, or `false` otherwise.
     */
    hasColGroup: function() {
        return this.thead.children('tr').length > 1;
    },

    /**
     * Adds and sets up an invisible row for internal purposes.
     * @protected
     */
    addGhostRow: function() {
        var firstRow = this.tbody.find('tr:first');
        if(firstRow.hasClass('ui-datatable-empty-message')) {
            return;
        }

        var columnsOfFirstRow = firstRow.children('td'),
        dataColumnsCount = columnsOfFirstRow.length,
        columnMarkup = '';

        for(var i = 0; i < dataColumnsCount; i++) {
            var colWidth = columnsOfFirstRow.eq(i).width() + 1,
            id = this.id + '_ghost_' + i;

            if (this.resizableState) {
                colWidth = this.findColWidthInResizableState(id) || colWidth;
            }

            columnMarkup += '<th id="' + id + '" style="height:0px;border-bottom-width: 0px;border-top-width: 0px;padding-top: 0px;padding-bottom: 0px;outline: 0 none; width:' + colWidth + 'px" class="ui-resizable-column"></th>';
        }

        this.thead.prepend('<tr>' + columnMarkup + '</tr>');

        if(this.cfg.scrollable) {
            this.theadClone.prepend('<tr>' + columnMarkup + '</tr>');
            this.footerTable.children('tfoot').prepend('<tr>' + columnMarkup + '</tr>');
        }
    },

    /**
     * Finds the group resizer element for the given drag event data.
     * @protected
     * @param {JQueryUI.DraggableEventUIParams} ui Data for the drag event.
     * @return {JQuery|null} The resizer DOM element.
     */
    findGroupResizer: function(ui) {
        for(var i = 0; i < this.groupResizers.length; i++) {
            var groupResizer = this.groupResizers.eq(i);
            if(groupResizer.offset().left === ui.helper.data('originalposition').left) {
                return groupResizer;
            }
        }

        return null;
    },

    /**
     * Adds the resizers for change the width of a column of this data table.
     * @protected
     */
    addResizers: function() {
        var resizableColumns = this.thead.find('> tr > th.ui-resizable-column');
        resizableColumns.prepend('<span class="ui-column-resizer">&nbsp;</span>');

        if(this.cfg.resizeMode === 'fit') {
            resizableColumns.filter(':last-child').children('span.ui-column-resizer').hide();
        }

        if(this.hasColumnGroup) {
            this.groupResizers = this.thead.find('> tr:first > th > .ui-column-resizer');
        }
    },

    /**
     * Resizes this data table, row, or columns in response to a drag event of a resizer element.
     * @protected
     * @param {JQuery.TriggeredEvent} event Event triggered for the drag.
     * @param {JQueryUI.DraggableEventUIParams} ui Data for the drag event.
     */
    resize: function(event, ui) {
        var columnHeader, nextColumnHeader, change = null, newWidth = null, nextColumnWidth = null,
        expandMode = (this.cfg.resizeMode === 'expand'),
        table = this.thead.parent(),
        $this = this;

        if(this.hasColumnGroup) {
            var groupResizer = this.findGroupResizer(ui);
            if(!groupResizer) {
                return;
            }

            columnHeader = groupResizer.parent();
        }
        else {
            columnHeader = ui.helper.parent();
        }

        var title = columnHeader.children('.ui-column-title');
        if(PrimeFaces.env.isIE()) {
            title.css('display', 'none');
        }

        var nextColumnHeader = columnHeader.nextAll(':visible:first');

        if(this.cfg.liveResize) {
            change = columnHeader.outerWidth() - (event.pageX - columnHeader.offset().left),
            newWidth = (columnHeader.width() - change),
            nextColumnWidth = (nextColumnHeader.width() + change);
        }
        else {
            change = (ui.position.left - ui.originalPosition.left),
            newWidth = (columnHeader.width() + change),
            nextColumnWidth = (nextColumnHeader.width() - change);
        }

        var minWidth = parseInt(columnHeader.css('min-width'));
        minWidth = (minWidth == 0) ? 15 : minWidth;

        if(PrimeFaces.env.isIE()) {
            title.css('display', '');
        }

        if((newWidth > minWidth && nextColumnWidth > minWidth) || (expandMode && newWidth > minWidth)) {
            if(expandMode) {
                table.width(table.width() + change);
                setTimeout(function() {
                    columnHeader.width(newWidth);
                    $this.updateResizableState(columnHeader, nextColumnHeader, table, newWidth, null);
                }, 1);
            }
            else {
                columnHeader.width(newWidth);
                nextColumnHeader.width(nextColumnWidth);
                this.updateResizableState(columnHeader, nextColumnHeader, table, newWidth, nextColumnWidth);
            }

            if(this.cfg.scrollable) {
                var cloneTable = this.theadClone.parent(),
                colIndex = columnHeader.index();

                if(expandMode) {
                    //body
                    cloneTable.width(cloneTable.width() + change);

                    //footer
                    this.footerTable.width(this.footerTable.width() + change);

                    setTimeout(function() {
                        if($this.hasColumnGroup) {
                            $this.theadClone.find('> tr:first').children('th').eq(colIndex).width(newWidth);            //body
                            $this.footerTable.find('> tfoot > tr:first').children('th').eq(colIndex).width(newWidth);   //footer
                        }
                        else {
                            $this.theadClone.find(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).width(newWidth);   //body
                            $this.footerCols.eq(colIndex).width(newWidth);                                                          //footer
                        }
                    }, 1);
                }
                else {
                    if(this.hasColumnGroup) {
                        //body
                        this.theadClone.find('> tr:first').children('th').eq(colIndex).width(newWidth);
                        this.theadClone.find('> tr:first').children('th').eq(colIndex + 1).width(nextColumnWidth);

                        //footer
                        this.footerTable.find('> tfoot > tr:first').children('th').eq(colIndex).width(newWidth);
                        this.footerTable.find('> tfoot > tr:first').children('th').eq(colIndex + 1).width(nextColumnWidth);
                    }
                    else {
                        //body
                        this.theadClone.find(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).width(newWidth);
                        this.theadClone.find(PrimeFaces.escapeClientId(nextColumnHeader.attr('id') + '_clone')).width(nextColumnWidth);

                        //footer
                        if(this.footerCols.length > 0) {
                            var footerCol = this.footerCols.eq(colIndex),
                            nextFooterCol = footerCol.next();

                            footerCol.width(newWidth);
                            nextFooterCol.width(nextColumnWidth);
                        }
                    }
                }
            }
        }
    },

    /**
     * Remove given row from the list of selected rows.
     * @private
     * @param {string} rowKey Key of the row to remove.
     */
    removeSelection: function(rowKey) {
        if(this.selection.includes('@all')) {
            // GitHub #3535 if @all was previously selected just select values on page
            this.clearSelection();
            var rows = this.tbody.children('tr');
            for(var i = 0; i < rows.length; i++) {
                var rowMeta = this.getRowMeta(rows.eq(i));
                if(rowMeta.key !== rowKey) {
                    this.addSelection(rowMeta.key);
                }
            }
        }
        else {
            this.selection = $.grep(this.selection, function(value) {
                return value !== rowKey;
            });
        }
    },

    /**
     * Adds given row to the list of selected rows.
     * @private
     * @param {number} rowKey Key of the row to add.
     */
    addSelection: function(rowKey) {
        if(!this.isSelected(rowKey)) {
            this.selection.push(rowKey);
        }
    },

    /**
     * Checks whether the given row is currently selected.
     * @param {string} rowKey The key of a row from this data table.
     * @return {boolean} `true` if the given row is currently selected, or `false` otherwise.
     */
    isSelected: function(rowKey) {
        return PrimeFaces.inArray(this.selection, rowKey);
    },

    /**
     * Finds the index and the row key for the given row.
     * @param {JQuery} row The element (`TR`) of a row of this data table.
     * @return {PrimeFaces.widget.DataTable.RowMeta} The meta for the row with the index and the row key.
     */
    getRowMeta: function(row) {
        var meta = {
            index: row.data('ri'),
            key:  row.attr('data-rk')
        };

        return meta;
    },

    /**
     * Sets up all event listeners required for making column draggable and reorderable.
     * @private
     */
    setupDraggableColumns: function() {
        this.orderStateHolder = $(this.jqId + '_columnOrder');
        this.saveColumnOrder();

        this.dragIndicatorTop = $('<span class="ui-icon ui-icon-arrowthick-1-s" style="position:absolute"></span>').hide().appendTo(this.jq);
        this.dragIndicatorBottom = $('<span class="ui-icon ui-icon-arrowthick-1-n" style="position:absolute"></span>').hide().appendTo(this.jq);

        var $this = this;

        $(this.jqId + ' thead th.ui-draggable-column').draggable({
            appendTo: 'body',
            opacity: 0.75,
            cursor: 'move',
            scope: this.id,
            cancel: ':input,.ui-column-resizer',
            start: function(event, ui) {
                ui.helper.css('z-index', PrimeFaces.nextZindex());
            },
            drag: function(event, ui) {
                var droppable = ui.helper.data('droppable-column');

                if(droppable) {
                    var droppableOffset = droppable.offset(),
                    topArrowY = droppableOffset.top - 10,
                    bottomArrowY = droppableOffset.top + droppable.height() + 8,
                    arrowX = null;

                    //calculate coordinates of arrow depending on mouse location
                    if(event.originalEvent.pageX >= droppableOffset.left + (droppable.width() / 2)) {
                        var nextDroppable = droppable.next();
                        if(nextDroppable.length == 1)
                            arrowX = nextDroppable.offset().left - 9;
                        else
                            arrowX = droppable.offset().left + droppable.innerWidth() - 9;

                        ui.helper.data('drop-location', 1);     //right
                    }
                    else {
                        arrowX = droppableOffset.left  - 9;
                        ui.helper.data('drop-location', -1);    //left
                    }

                    $this.dragIndicatorTop.offset({
                        'left': arrowX,
                        'top': topArrowY - 3
                    }).show();

                    $this.dragIndicatorBottom.offset({
                        'left': arrowX,
                        'top': bottomArrowY - 3
                    }).show();
                }
            },
            stop: function(event, ui) {
                //hide dnd arrows
                $this.dragIndicatorTop.css({
                    'left':'0px',
                    'top':'0px'
                }).hide();

                $this.dragIndicatorBottom.css({
                    'left':'0px',
                    'top':'0px'
                }).hide();
            },
            helper: function() {
                var header = $(this),
                helper = $('<div class="ui-widget ui-state-default" style="padding:4px 10px;text-align:center;"></div>');

                helper.width(header.width());
                helper.height(header.height());

                helper.html(header.html());

                return helper.get(0);
            }
        }).droppable({
            hoverClass:'ui-state-highlight',
            tolerance:'pointer',
            scope: this.id,
            over: function(event, ui) {
                ui.helper.data('droppable-column', $(this));
            },
            drop: function(event, ui) {
                var draggedColumnHeader = ui.draggable,
                dropLocation = ui.helper.data('drop-location'),
                droppedColumnHeader =  $(this),
                draggedColumnFooter = null,
                droppedColumnFooter = null;

                var draggedCells = $this.tbody.find('> tr:not(.ui-expanded-row-content) > td:nth-child(' + (draggedColumnHeader.index() + 1) + ')'),
                droppedCells = $this.tbody.find('> tr:not(.ui-expanded-row-content) > td:nth-child(' + (droppedColumnHeader.index() + 1) + ')');

                if($this.tfoot.length) {
                    var footerColumns = $this.tfoot.find('> tr > td'),
                    draggedColumnFooter = footerColumns.eq(draggedColumnHeader.index()),
                    droppedColumnFooter = footerColumns.eq(droppedColumnHeader.index());
                }

                //drop right
                if(dropLocation > 0) {
                    if($this.cfg.resizableColumns) {
                        if(droppedColumnHeader.next().length) {
                            droppedColumnHeader.children('span.ui-column-resizer').show();
                            draggedColumnHeader.children('span.ui-column-resizer').hide();
                        }
                    }

                    draggedColumnHeader.insertAfter(droppedColumnHeader);

                    draggedCells.each(function(i, item) {
                        $(this).insertAfter(droppedCells.eq(i));
                    });

                    if(draggedColumnFooter && droppedColumnFooter) {
                        draggedColumnFooter.insertAfter(droppedColumnFooter);
                    }

                    //sync clone
                    if($this.cfg.scrollable) {
                        var draggedColumnClone = $(document.getElementById(draggedColumnHeader.attr('id') + '_clone')),
                        droppedColumnClone = $(document.getElementById(droppedColumnHeader.attr('id') + '_clone'));
                        draggedColumnClone.insertAfter(droppedColumnClone);
                    }
                }
                //drop left
                else {
                    draggedColumnHeader.insertBefore(droppedColumnHeader);

                    draggedCells.each(function(i, item) {
                        $(this).insertBefore(droppedCells.eq(i));
                    });

                    if(draggedColumnFooter && droppedColumnFooter) {
                        draggedColumnFooter.insertBefore(droppedColumnFooter);
                    }

                    //sync clone
                    if($this.cfg.scrollable) {
                        var draggedColumnClone = $(document.getElementById(draggedColumnHeader.attr('id') + '_clone')),
                        droppedColumnClone = $(document.getElementById(droppedColumnHeader.attr('id') + '_clone'));
                        draggedColumnClone.insertBefore(droppedColumnClone);
                    }
                }

                //save order
                $this.saveColumnOrder();

                //fire colReorder event
                if($this.hasBehavior('colReorder')) {
                    var ext = null;

                    if($this.cfg.multiViewState) {
                        ext = {
                            params: [{name: this.id + '_encodeFeature', value: true}]
                        };
                    }

                    $this.callBehavior('colReorder', ext);
                }
            }
        });
    },

    /**
     * Saves the current column order, used to preserve the state between AJAX updates etc.
     * @protected
     */
    saveColumnOrder: function() {
        var columnIds = [],
        columns = $(this.jqId + ' thead:first th');

        columns.each(function(i, item) {
            columnIds.push($(item).attr('id'));
        });

        this.orderStateHolder.val(columnIds.join(','));
    },

    /**
     * Makes the rows of this data table draggable via JQueryUI.
     * @private
     */
    makeRowsDraggable: function() {
        var $this = this,
        draggableHandle = this.cfg.rowDragSelector||'td,span:not(.ui-c)';

        this.tbody.sortable({
            placeholder: 'ui-datatable-rowordering ui-state-active',
            cursor: 'move',
            handle: draggableHandle,
            appendTo: document.body,
            start: function(event, ui) {
                ui.helper.css('z-index', PrimeFaces.nextZindex());
            },
            helper: function(event, ui) {
                var cells = ui.children(),
                helper = $('<div class="ui-datatable ui-widget"><table><tbody class="ui-datatable-data"></tbody></table></div>'),
                helperRow = ui.clone(),
                helperCells = helperRow.children();

                for(var i = 0; i < helperCells.length; i++) {
                    var helperCell = helperCells.eq(i);
                    helperCell.width(cells.eq(i).width());
                    // #5584 reflow must remove column title span
                    helperCell.children().remove('.ui-column-title');
                }

                helperRow.appendTo(helper.find('tbody'));

                return helper;
            },
            update: function(event, ui) {
                var fromIndex = ui.item.data('ri'),
                fromNode = ui.item;
                itemIndex = ui.item.index(),
                toIndex = $this.paginator ? $this.paginator.getFirst() + itemIndex : itemIndex;
                isDirectionUp = fromIndex >= toIndex;

                // #5296 must not count header group rows
                // #6557 must not count expanded rows
                if (isDirectionUp) {
                    for (i = 0; i <= toIndex; i++) {
                        fromNode = fromNode.next('tr');
                        if (fromNode.hasClass('ui-rowgroup-header') || fromNode.hasClass('ui-expanded-row-content')){
                            toIndex--;
                        }
                    }
                } else {
                    fromNode.prevAll('tr').each(function() {
                        var node = $(this);
                        if (node.hasClass('ui-rowgroup-header') || node.hasClass('ui-expanded-row-content')){
                            toIndex--;
                        }
                    });
                }
                toIndex = Math.max(toIndex, 0);

                $this.syncRowParity();

                var options = {
                    source: $this.id,
                    process: $this.id,
                    params: [
                        {name: $this.id + '_rowreorder', value: true},
                        {name: $this.id + '_fromIndex', value: fromIndex},
                        {name: $this.id + '_toIndex', value: toIndex},
                        {name: this.id + '_skipChildren', value: true}
                    ]
                }

                if($this.hasBehavior('rowReorder')) {
                    $this.callBehavior('rowReorder', options);
                }
                else {
                    PrimeFaces.ajax.Request.handle(options);
                }
            },
            change: function(event, ui) {
                if($this.cfg.scrollable) {
                    PrimeFaces.scrollInView($this.scrollBody, ui.placeholder);
                }
            }
        });
    },

    /**
     * Sets the style class on each, depending whether it is an even-numbered or odd-numbered row.
     * @private
     */
    syncRowParity: function() {
        var rows = this.tbody.children('tr.ui-widget-content'),
        first = this.paginator ? this.paginator.getFirst(): 0;

        for(var i = first; i < rows.length; i++) {
            var row = rows.eq(i);

            row.data('ri', i).removeClass('ui-datatable-even ui-datatable-odd');

            if(i % 2 === 0)
                row.addClass('ui-datatable-even');
            else
                row.addClass('ui-datatable-odd');

        }
    },

    /**
     * Checks whether this data table has got any rows. When there are no rows, usually the message `no items found` is
     * shown.
     * @return {boolean} `true` if this data table has got no rows, `false` otherwise.
     */
    isEmpty: function() {
        return this.tbody.children('tr.ui-datatable-empty-message').length === 1;
    },

    /**
     * Finds the number of rows that are selected.
     * @return {number} The number of rows that are currently selected.
     */
    getSelectedRowsCount: function() {
        return this.isSelectionEnabled() ? this.selection.length : 0;
    },

    /**
     * Updates the `check all` checkbox in the header of this data table.
     * @private
     */
    updateHeaderCheckbox: function() {
        if(this.isEmpty()) {
            this.uncheckHeaderCheckbox();
            this.disableHeaderCheckbox();
        }
        else if(!this.cfg.selectionPageOnly) {
            if(this.selection.includes('@all')) {
                this.enableHeaderCheckbox();
                this.checkHeaderCheckbox();
            }
        }
        else {
            var checkboxes, selectedCheckboxes, enabledCheckboxes, disabledCheckboxes;

            if(this.cfg.nativeElements) {
                checkboxes = this.tbody.find('> tr > td.ui-selection-column > :checkbox');
                enabledCheckboxes = checkboxes.filter(':enabled');
                disabledCheckboxes = checkboxes.filter(':disabled');
                selectedCheckboxes = enabledCheckboxes.filter(':checked');
            }
            else {
                checkboxes = this.tbody.find('> tr > td.ui-selection-column > div.ui-chkbox > .ui-chkbox-box');
                enabledCheckboxes = checkboxes.filter(':not(.ui-state-disabled)');
                disabledCheckboxes = checkboxes.filter('.ui-state-disabled');
                selectedCheckboxes = checkboxes.filter("div[aria-checked='true']");
            }

            if(enabledCheckboxes.length && enabledCheckboxes.length === selectedCheckboxes.length)
               this.checkHeaderCheckbox();
            else
               this.uncheckHeaderCheckbox();

            if(checkboxes.length === disabledCheckboxes.length)
               this.disableHeaderCheckbox();
            else
               this.enableHeaderCheckbox();
        }
    },

    /**
     * Checks the `select all` checkbox in the header of this data table.
     * @private
     */
    checkHeaderCheckbox: function() {
        if(this.cfg.nativeElements) {
            this.checkAllToggler.prop('checked', true);
        }
        else {
            this.checkAllToggler.addClass('ui-state-active').children('span.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
            this.checkAllToggler.attr('aria-checked', true);
        }
    },

    /**
     * Unchecks the `select all` checkbox in the header of this data table.
     * @private
     */
    uncheckHeaderCheckbox: function() {
        if(this.cfg.nativeElements) {
            this.checkAllToggler.prop('checked', false);
        }
        else {
            this.checkAllToggler.removeClass('ui-state-active').children('span.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
            this.checkAllToggler.attr('aria-checked', false);
        }
    },

    /**
     * Disables the `select all` checkbox in the header of this data table.
     * @private
     */
    disableHeaderCheckbox: function() {
        if(this.cfg.nativeElements)
            this.checkAllToggler.prop('disabled', true);
        else
            this.checkAllToggler.addClass('ui-state-disabled');
    },

    /**
     * Enables the `select all` checkbox in the header of this data table.
     * @private
     */
    enableHeaderCheckbox: function() {
        if(this.cfg.nativeElements)
            this.checkAllToggler.prop('disabled', false);
        else
            this.checkAllToggler.removeClass('ui-state-disabled');
    },

    /**
     * Applies the styling and event listeners required for the sticky headers feature.
     * @private
     */
    setupStickyHeader: function() {
        var table = this.thead.parent(),
        offset = table.offset(),
        win = $(window),
        $this = this,
        orginTableContent = this.jq.find('> .ui-datatable-tablewrapper > table'),
        fixedElementsOnTop = this.cfg.stickyTopAt ? $(this.cfg.stickyTopAt) : null,
        fixedElementsHeight = 0;

        if (fixedElementsOnTop && fixedElementsOnTop.length) {
            for (var i = 0; i < fixedElementsOnTop.length; i++) {
                fixedElementsHeight += fixedElementsOnTop.eq(i).outerHeight();
            }
        }

        this.stickyContainer = $('<div class="ui-datatable ui-datatable-sticky ui-widget"><table></table></div>');
        this.clone = this.thead.clone(false);
        this.stickyContainer.children('table').append(this.thead);
        table.prepend(this.clone);

        this.stickyContainer.css({
            position: 'absolute',
            width: table.outerWidth() + 'px',
            top: offset.top + 'px',
            left: offset.left + 'px',
            'z-index': PrimeFaces.nextZindex()
        });

        this.jq.prepend(this.stickyContainer);

        if(this.cfg.resizableColumns) {
            this.relativeHeight = 0;
        }

        PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id, function() {
            var scrollTop = win.scrollTop(),
            tableOffset = table.offset();

            if(scrollTop + fixedElementsHeight > tableOffset.top) {
                $this.stickyContainer.css({
                                        'position': 'fixed',
                                        'top': fixedElementsHeight + 'px'
                                    })
                                    .addClass('ui-shadow ui-sticky');

                if($this.cfg.resizableColumns) {
                    $this.relativeHeight = (scrollTop + fixedElementsHeight) - tableOffset.top;
                }

                if(scrollTop + fixedElementsHeight >= (tableOffset.top + $this.tbody.height()))
                    $this.stickyContainer.hide();
                else
                    $this.stickyContainer.show();
            }
            else {
                $this.stickyContainer.css({
                                        'position': 'absolute',
                                        'top': tableOffset.top + 'px'
                                    })
                                    .removeClass('ui-shadow ui-sticky');

                if($this.stickyContainer.is(':hidden')) {
                    $this.stickyContainer.show();
                }

                if($this.cfg.resizableColumns) {
                    $this.relativeHeight = 0;
                }
            }
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.sticky-' + this.id, null, function(e) {
            var _delay = e.data.delay;

            if (_delay !== null && typeof _delay === 'number' && _delay > -1) {
                if ($this.resizeTimeout) {
                    clearTimeout($this.resizeTimeout);
                }

                $this.stickyContainer.hide();
                $this.resizeTimeout = setTimeout(function() {
                    $this.stickyContainer.css('left', orginTableContent.offset().left + 'px');
                    $this.stickyContainer.width(table.outerWidth());
                    $this.stickyContainer.show();
                }, _delay);
            }
            else {
                $this.stickyContainer.width(table.outerWidth());
            }
        }, { delay: null });

        //filter support
        this.clone.find('.ui-column-filter').prop('disabled', true);
    },

    /**
     * Initializes the expansion state
     * @private
     */
    initRowExpansion: function() {
        var $this = this;

        this.expansionHolder = $(this.jqId + '_rowExpansionState');
        this.loadedExpansionRows = this.tbody.children('.ui-expanded-row-content').prev().map(function() {
            return $this.getRowMeta($(this)).index;
        }).get();

        this.writeRowExpansions();
    },

    /**
     * Write row expansion state.
     * @private
     */
    writeRowExpansions: function() {
        this.expansionHolder.val(this.loadedExpansionRows.join(','));
    },

    /**
     * Detect if row expansion for this row has been loaded and if not load it.
     * @protected
     * @param {number} rowIndex The row index to check for expansion
     */
    rowExpansionLoaded: function(rowIndex) {
        if(!PrimeFaces.inArray(this.loadedExpansionRows, rowIndex)) {
            this.loadedExpansionRows.push(rowIndex);
            this.writeRowExpansions();
        }
    },

    /**
     * Finds the body of this data table with the property that the user can focus it.
     * @protected
     * @return {JQuery} The body of this data table.
     */
    getFocusableTbody: function() {
        return this.tbody;
    },

    /**
     * Removes the current clone of the table header from the DOM, and creates a new clone.
     * @private
     */
    reclone: function() {
        this.clone.remove();
        this.clone = this.thead.clone(false);
        this.jq.find('.ui-datatable-tablewrapper > table').prepend(this.clone);
    },

    /**
     * Fetches the last row from the backend and inserts a row instead of updating the table itself.
     */
    addRow: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [{name: this.id + '_addrow', value: true},
                    {name: this.id + '_skipChildren', value: true},
                    {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        this.tbody.append(content);
                    }
                });

                if ($this.isEmpty()) {
                    $this.tbody.children('tr.ui-datatable-empty-message').remove();
                }

                return true;
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Clears all cached rows so that they are loaded from the server the next time they are requested.
     * @private
     */
    clearCacheMap: function() {
        this.cacheMap = {};
    },

    /**
     * Loads the data for the given page and displays it. When some rows exist in the cache, do not reload them from the
     * server.
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new values for the current page and the rows
     * per page count.
     * @private
     */
    loadDataWithCache: function(newState) {
        var isRppChanged = false;
        if(this.cacheRows != newState.rows) {
            this.clearCacheMap();
            this.cacheRows = newState.rows;
            isRppChanged = true;
        }

        var pageFirst = newState.first,
            nextPageFirst = newState.rows + pageFirst,
            lastPageFirst = this.cfg.paginator.pageCount * newState.rows,
            hasNextPage = (!this.cacheMap[nextPageFirst]) && nextPageFirst < lastPageFirst;

        if(this.cacheMap[pageFirst] && !isRppChanged) {
            this.updateData(this.cacheMap[pageFirst]);
            this.paginator.cfg.page = newState.page;
            this.paginator.updateUI();

            if(!hasNextPage) {
                this.updatePageState(newState);
            }
        }
        else {
            this.paginate(newState);
        }

        if(hasNextPage) {
            this.fetchNextPage(newState);
        }
    },

    /**
     * Reflow mode is a responsive mode to display columns as stacked depending on screen size. Updates the reflow for
     * the given header.
     * @private
     * @param {JQuery} columnHeader Header of a column to update.
     * @param {number} sortOrder Sort order of the column.
     */
    updateReflowDD: function(columnHeader, sortOrder) {
        if(this.reflowDD && this.cfg.reflow) {
            var options = this.reflowDD.children('option'),
            orderIndex = sortOrder > 0 ? 0 : 1;
            var header = columnHeader.text();
            var filterby = header.indexOf("Filter by");
            if (filterby !== -1) {
                header = header.substring(0, filterby);
            }
            header = $.escapeSelector(header);

            options.each(function() {
                var optionText = $.escapeSelector(this.text);
                this.selected = optionText.startsWith(header) && this.value.endsWith("_" + orderIndex);
            });
        }
    },

    /**
     * When row grouping is enabled, groups all rows accordingly.
     * @protected
     */
    groupRows: function() {
        var rows = this.tbody.children('tr');
        for(var i = 0; i < this.cfg.groupColumnIndexes.length; i++) {
            this.groupRow(this.cfg.groupColumnIndexes[i], rows);
        }

        rows.children('td.ui-duplicated-column').remove();
    },

    /**
     * Called by `groupRows`, this method performs the grouping of a single set of rows that belong to one row group.
     * @private
     * @param {number} colIndex Index of the column to group.
     * @param {JQuery} rows Rows to group into one row group.
     */
    groupRow: function(colIndex, rows) {
        var groupStartIndex = null, rowGroupCellData = null, rowGroupCount = null;

        for(var i = 0; i < rows.length; i++) {
            var row = rows.eq(i);
            var column = row.children('td').eq(colIndex);
            var columnData = column.text();
            if(rowGroupCellData != columnData) {
                groupStartIndex = i;
                rowGroupCellData = columnData;
                rowGroupCount = 1;

                if (this.cfg.liveScroll && column[0].hasAttribute('rowspan')) {
                    rowGroupCount = parseInt(column.attr('rowspan'));
                    i += rowGroupCount - 1;
                }
            }
            else {
                column.addClass('ui-duplicated-column');
                rowGroupCount++;
            }

            if(groupStartIndex != null && rowGroupCount > 1) {
                rows.eq(groupStartIndex).children('td').eq(colIndex).attr('rowspan', rowGroupCount);
            }
        }
    },

    /**
     * Sets up the event handlers for row group events.
     * @protected
     */
    bindToggleRowGroupEvents: function() {
        var expandableRows = this.tbody.children('tr.ui-rowgroup-header'),
            toggler = expandableRows.find('> td:first > a.ui-rowgroup-toggler');

        toggler.off('click.dataTable-rowgrouptoggler').on('click.dataTable-rowgrouptoggler', function(e) {
           var link = $(this),
           togglerIcon = link.children('.ui-rowgroup-toggler-icon'),
           parentRow = link.closest('tr.ui-rowgroup-header');

           if(togglerIcon.hasClass('ui-icon-circle-triangle-s')) {
               link.attr('aria-expanded', false);
               togglerIcon.addClass('ui-icon-circle-triangle-e').removeClass('ui-icon-circle-triangle-s');
               parentRow.nextUntil('tr.ui-rowgroup-header').hide();
           }
           else {
               link.attr('aria-expanded', true);
               togglerIcon.addClass('ui-icon-circle-triangle-s').removeClass('ui-icon-circle-triangle-e');
               parentRow.nextUntil('tr.ui-rowgroup-header').show();
           }

           e.preventDefault();
        });
    },

    /**
     * Computes the `colspan value for the table rows.
     * @private
     * @return {number} The computed `colspan` value.
     */
    calculateColspan: function() {
        var visibleHeaderColumns = this.thead.find('> tr:first th:not(.ui-helper-hidden)'),
            colSpanValue = 0;

        for(var i = 0; i < visibleHeaderColumns.length; i++) {
            var column = visibleHeaderColumns.eq(i);
            if(column.is('[colspan]')) {
                colSpanValue += parseInt(column.attr('colspan'));
            }
            else {
                colSpanValue++;
            }
        }

        return colSpanValue;
    },

    /**
     * Updates the `colspan` attribute of the given row.
     * @private
     * @param {JQuery} row A row to update.
     * @param {number} [colspanValue] The new `colspan` value. If not given, computes the value automatically.
     */
    updateColspan: function(row, colspanValue) {
        row.children('td').attr('colspan', colspanValue || this.calculateColspan());
    },

    /**
     * Updates the colspan attribute for the message shown when no rows are available.
     * @private
     */
    updateEmptyColspan: function() {
        var emptyRow = this.tbody.children('tr:first');
        if(emptyRow && emptyRow.hasClass('ui-datatable-empty-message')) {
            this.updateColspan(emptyRow);
        }
    },

    /**
     * Updates the `colspan` attributes of all expanded rows.
     * @private
     */
    updateExpandedRowsColspan: function() {
        var colspanValue = this.calculateColspan(),
            $this = this;
        this.getExpandedRows().each(function() {
            $this.updateColspan($(this).next('.ui-expanded-row-content'), colspanValue);
        });
    },

    /**
     * Computes and saves the resizable state of this data table, ie. which columns have got which width. May be used
     * later to restore the current column width after an AJAX update.
     * @private
     * @param {JQuery} columnHeader Element of a column header of this data table.
     * @param {JQuery} nextColumnHeader Element of the column header next to the given column header.
     * @param {JQuery} table The element for this data table.
     * @param {number} newWidth New width to be applied.
     * @param {number | null} nextColumnWidth Width of the column next to the given column header.
     */
    updateResizableState: function(columnHeader, nextColumnHeader, table, newWidth, nextColumnWidth) {
        var expandMode = (this.cfg.resizeMode === 'expand'),
        currentColumnId = columnHeader.attr('id'),
        nextColumnId = nextColumnHeader.attr('id'),
        tableId = this.id + "_tableWidthState",
        currentColumnState = currentColumnId + '_' + newWidth,
        nextColumnState = nextColumnId + '_' + nextColumnWidth,
        tableState = tableId + '_' + parseInt(table.css('width')),
        currentColumnMatch = false,
        nextColumnMatch = false,
        tableMatch = false;

        for(var i = 0; i < this.resizableState.length; i++) {
            var state = this.resizableState[i];
            if(state.indexOf(currentColumnId) === 0) {
                this.resizableState[i] = currentColumnState;
                currentColumnMatch = true;
            }
            else if(!expandMode && state.indexOf(nextColumnId) === 0) {
                this.resizableState[i] = nextColumnState;
                nextColumnMatch = true;
            }
            else if(expandMode && state.indexOf(tableId) === 0) {
                this.resizableState[i] = tableState;
                tableMatch = true;
            }
        }

        if(!currentColumnMatch) {
            this.resizableState.push(currentColumnState);
        }

        if(!expandMode && !nextColumnMatch) {
            this.resizableState.push(nextColumnState);
        }

        if(expandMode && !tableMatch) {
            this.resizableState.push(tableState);
        }

        this.resizableStateHolder.val(this.resizableState.join(','));
    },

    /**
     * Finds the saved width of the given column. The width of resizable columns may be saved to restore it after an
     * AJAX update.
     * @private
     * @param {string} id ID of a column
     * @return {string | undefined} The saved width of the given column in pixels. `undefined` when the given column
     * does not exist.
     */
    findColWidthInResizableState: function(id) {
        for (var i = 0; i < this.resizableState.length; i++) {
            var state = this.resizableState[i];
            if (state.indexOf(id) === 0) {
                return state.substring(state.lastIndexOf('_') + 1, state.length);
            }
        }

        return null;
    },

    /**
     * Updates some style classes for all columns.
     * @private
     */
    updateColumnsView: function() {
        if(this.isEmpty()) {
            return;
        }

        // update the visibility of columns but ignore expanded rows
        if(this.headers) {
            for(var i = 0; i < this.headers.length; i++) {
                var header = this.headers.eq(i),
                    col = this.tbody.find('> tr:not(.ui-expanded-row-content) > td:nth-child(' + (header.index() + 1) + ')');

                if(header.hasClass('ui-helper-hidden')) {
                    col.addClass('ui-helper-hidden');
                }
                else {
                    col.removeClass('ui-helper-hidden');
                }
            }
        }

        // update the colspan of the expanded rows
        if(this.cfg.expansion) {
            this.updateExpandedRowsColspan();
        }
    },

    /**
     * Resets the scroll state of the body to a non-scrolled state.
     * @protected
     */
    resetVirtualScrollBody: function() {
        this.bodyTable.css('top', '0px');
        this.scrollBody.scrollTop(0);
        this.clearScrollState();
    }

});

/**
 * __PrimeFaces DataTable with Frozen Columns Widget__
 *
 * @prop {JQuery} frozenBody The DOM element for the frozen body.
 * @prop {JQuery} frozenBodyTable The DOM element for the frozen body TABLE.
 * @prop {JQuery} frozenContainer The DOM element for the container of the frozen table.
 * @prop {JQuery} frozenFooter The DOM element for the frozen footer.
 * @prop {JQuery} frozenFooterCols The DOM elements for the frozen columns of the footer.
 * @prop {JQuery} frozenFooterTable The DOM element for the frozen data table footer TABLE.
 * @prop {JQuery} frozenGroupResizers The DOM element for the frozen group resizers of the footer.
 * @prop {JQuery} frozenHeader The DOM element for the frozen header.
 * @prop {JQuery} frozenLayout The DOM element for the frozen layout container.
 * @prop {JQuery} frozenTbody The DOM element for the header TBODY.
 * @prop {JQuery} frozenThead The DOM element for the header THEAD.
 * @prop {JQuery} frozenTheadClone The DOM element for the clone of the frozen THEAD.
 * @prop {JQuery} scrollBodyTable The DOM element for the TABLE of the scrollable body.
 * @prop {JQuery} scrollContainer The DOM element for the container of the scrollable body.
 * @prop {JQuery} scrollFooterCols The DOM element for the scrollable columns of the footer.
 * @prop {JQuery} scrollFooterTable The DOM element for the TABLE of the scrollable footer.
 * @prop {JQuery} scrollGroupResizers The DOM element for the group resizers of the scrollable body.
 * @prop {JQuery} scrollHeaderTable The DOM element for the TABLE of the scrollable header.
 * @prop {JQuery} scrollLayout The DOM element for the scrollable layout container.
 * @prop {JQuery} scrollTbody The DOM element for the scrollable TBODY.
 * @prop {JQuery} scrollThead The DOM element for the scrollable THEAD.
 * @prop {JQuery} scrollTheadClone The DOM element for the clone of the scrollable THEAD.
 *
 * @interface {PrimeFaces.widget.FrozenDataTableCfg} cfg The configuration for the {@link  FrozenDataTable| FrozenDataTable 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.DataTableCfg} cfg
 */
PrimeFaces.widget.FrozenDataTable = PrimeFaces.widget.DataTable.extend({

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    setupScrolling: function() {
        this.scrollLayout = this.jq.find('> table > tbody > tr > td.ui-datatable-frozenlayout-right');
        this.frozenLayout = this.jq.find('> table > tbody > tr > td.ui-datatable-frozenlayout-left');
        this.scrollContainer = this.jq.find('> table > tbody > tr > td.ui-datatable-frozenlayout-right > .ui-datatable-scrollable-container');
        this.frozenContainer = this.jq.find('> table > tbody > tr > td.ui-datatable-frozenlayout-left > .ui-datatable-frozen-container');
        this.scrollHeader = this.scrollContainer.children('.ui-datatable-scrollable-header');
        this.scrollHeaderBox = this.scrollHeader.children('div.ui-datatable-scrollable-header-box');
        this.scrollBody = this.scrollContainer.children('.ui-datatable-scrollable-body');
        this.scrollFooter = this.scrollContainer.children('.ui-datatable-scrollable-footer');
        this.scrollFooterBox = this.scrollFooter.children('div.ui-datatable-scrollable-footer-box');
        this.scrollStateHolder = $(this.jqId + '_scrollState');
        this.scrollHeaderTable = this.scrollHeaderBox.children('table');
        this.scrollBodyTable = this.cfg.virtualScroll ? this.scrollBody.children('div').children('table') : this.scrollBody.children('table');
        this.scrollThead = this.thead.eq(1);
        this.scrollTbody = this.tbody.eq(1);
        this.scrollFooterTable = this.scrollFooterBox.children('table');
        this.scrollFooterCols = this.scrollFooter.find('> .ui-datatable-scrollable-footer-box > table > tfoot > tr > td');
        this.frozenHeader = this.frozenContainer.children('.ui-datatable-scrollable-header');
        this.frozenBody = this.frozenContainer.children('.ui-datatable-scrollable-body');
        this.frozenBodyTable = this.cfg.virtualScroll ? this.frozenBody.children('div').children('table') : this.frozenBody.children('table');
        this.frozenThead = this.thead.eq(0);
        this.frozenTbody = this.tbody.eq(0);
        this.frozenFooter = this.frozenContainer.children('.ui-datatable-scrollable-footer');
        this.frozenFooterTable = this.frozenFooter.find('> .ui-datatable-scrollable-footer-box > table');
        this.frozenFooterCols = this.frozenFooter.find('> .ui-datatable-scrollable-footer-box > table > tfoot > tr > td');
        this.percentageScrollHeight = this.cfg.scrollHeight && (this.cfg.scrollHeight.indexOf('%') !== -1);
        this.percentageScrollWidth = this.cfg.scrollWidth && (this.cfg.scrollWidth.indexOf('%') !== -1);

        this.frozenThead.find('> tr > th').addClass('ui-frozen-column');

        var $this = this,
        scrollBarWidth = this.getScrollbarWidth() + 'px',
        hScrollWidth = this.scrollBody[0].scrollWidth;

        if(this.cfg.scrollHeight) {
            if(this.percentageScrollHeight) {
                this.adjustScrollHeight();
            }

            if(this.hasVerticalOverflow()) {
                this.scrollHeaderBox.css('margin-right', scrollBarWidth);
                this.scrollFooterBox.css('margin-right', scrollBarWidth);
            }
        }

        if(this.cfg.selectionMode) {
            this.scrollTbody.removeAttr('tabindex');
        }

        this.fixColumnWidths();

        if(this.cfg.scrollWidth) {
            if(this.percentageScrollWidth)
                this.adjustScrollWidth();
            else
                this.setScrollWidth(parseInt(this.cfg.scrollWidth));

            if(this.hasVerticalOverflow()) {
                var browser = PrimeFaces.env.browser;
                if(browser.webkit === true || browser.mozilla === true)
                    this.frozenBody.append('<div style="height:' + scrollBarWidth + ';border:1px solid transparent"></div>');
                else
                    this.frozenBodyTable.css('margin-bottom', scrollBarWidth);
            }
        }

        this.cloneHead();

        if(this.cfg.liveScroll) {
            this.clearScrollState();
            this.scrollOffset = 0;
            this.cfg.liveScrollBuffer = (100 - this.cfg.liveScrollBuffer) / 100;
            this.shouldLiveScroll = true;
            this.loadingLiveScroll = false;
            this.allLoadedLiveScroll = $this.cfg.scrollStep >= $this.cfg.scrollLimit;
        }

        this.restoreScrollState();

        if(this.cfg.virtualScroll) {
            var row = this.scrollTbody.children('tr.ui-widget-content');
            if(row) {
                this.rowHeight = row.outerHeight();
                this.scrollBody.children('div').css('height', parseFloat((this.cfg.scrollLimit * this.rowHeight) + 'px'));
                this.frozenBody.children('div').css('height', parseFloat((this.cfg.scrollLimit * this.rowHeight) + 'px'));
            }

            if(!this.cfg.scrollHeight) {
                this.frozenBody.css('height', this.scrollBody.height());
            }
        }

        this.scrollBody.on('scroll.datatable', function() {
            var scrollLeft = $this.scrollBody.scrollLeft(),
            scrollTop = $this.scrollBody.scrollTop();

            if ($this.isRTL) {
                $this.scrollHeaderBox.css('margin-right', (scrollLeft - hScrollWidth + this.clientWidth) + 'px');
                $this.scrollFooterBox.css('margin-right', (scrollLeft - hScrollWidth + this.clientWidth) + 'px');
            }
            else {
                $this.scrollHeaderBox.css('margin-left', -scrollLeft + 'px');
                $this.scrollFooterBox.css('margin-left', -scrollLeft + 'px');
            }

            $this.frozenBody.scrollTop(scrollTop);

            if($this.cfg.virtualScroll) {
                var virtualScrollBody = this;

                clearTimeout($this.scrollTimeout);
                $this.scrollTimeout = setTimeout(function() {
                    var viewportHeight = $this.scrollBody.outerHeight(),
                    tableHeight = $this.scrollBodyTable.outerHeight(),
                    pageHeight = $this.rowHeight * $this.cfg.scrollStep,
                    virtualTableHeight = parseFloat(($this.cfg.scrollLimit * $this.rowHeight) + 'px'),
                    pageCount = (virtualTableHeight / pageHeight)||1;

                    if(virtualScrollBody.scrollTop + viewportHeight > parseFloat($this.scrollBodyTable.css('top')) + tableHeight || virtualScrollBody.scrollTop < parseFloat($this.scrollBodyTable.css('top'))) {
                        var page = Math.floor((virtualScrollBody.scrollTop * pageCount) / (virtualScrollBody.scrollHeight)) + 1;
                        $this.loadRowsWithVirtualScroll(page, function () {
                            $this.scrollBodyTable.css('top', ((page - 1) * pageHeight) + 'px');
                            $this.frozenBodyTable.css('top', ((page - 1) * pageHeight) + 'px');
                        });
                    }
                }, 200);
            }
            else if($this.shouldLiveScroll) {
                var scrollTop = Math.ceil(this.scrollTop),
                scrollHeight = this.scrollHeight,
                viewportHeight = this.clientHeight;

                if((scrollTop >= ((scrollHeight * $this.cfg.liveScrollBuffer) - (viewportHeight))) && $this.shouldLoadLiveScroll()) {
                    $this.loadLiveRows();
                }
            }

            $this.saveScrollState();
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.jq, function() {
            if ($this.percentageScrollHeight) {
                $this.adjustScrollHeight();
            }
            if ($this.percentageScrollWidth) {
                $this.adjustScrollWidth();
            }
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    cloneHead: function() {
        if (this.frozenTheadClone) {
            this.frozenTheadClone.remove();
        }
        this.frozenTheadClone = this.cloneTableHeader(this.frozenThead, this.frozenBodyTable);

        if (this.scrollTheadClone) {
            this.scrollTheadClone.remove();
        }
        this.scrollTheadClone = this.cloneTableHeader(this.scrollThead, this.scrollBodyTable);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @return {boolean}
     */
    hasVerticalOverflow: function() {
        return this.scrollBodyTable.outerHeight() > this.scrollBody.outerHeight();
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    adjustScrollHeight: function() {
        var relativeHeight = this.jq.parent().innerHeight() * (parseInt(this.cfg.scrollHeight) / 100),
        headerChilden = this.jq.children('.ui-datatable-header'),
        footerChilden = this.jq.children('.ui-datatable-footer'),
        tableHeaderHeight = (headerChilden.length > 0) ? headerChilden.outerHeight(true) : 0,
        tableFooterHeight = (footerChilden.length > 0) ? footerChilden.outerHeight(true) : 0,
        scrollersHeight = (this.scrollHeader.innerHeight() + this.scrollFooter.innerHeight()),
        paginatorsHeight = this.paginator ? this.paginator.getContainerHeight(true) : 0,
        height = (relativeHeight - (scrollersHeight + paginatorsHeight + tableHeaderHeight + tableFooterHeight));

        if(this.cfg.virtualScroll) {
            this.scrollBody.css('max-height', height + 'px');
            this.frozenBody.css('max-height', height + 'px');
        }
        else {
            this.scrollBody.height(height);
            this.frozenBody.height(height);
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    adjustScrollWidth: function() {
        var scrollLayoutWidth = this.jq.parent().innerWidth() - this.frozenLayout.innerWidth(),
        width = parseInt((scrollLayoutWidth * (parseInt(this.cfg.scrollWidth) / 100)));

        this.setScrollWidth(width);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {number} width
     */
    setScrollWidth: function(width) {
        this.scrollHeader.width(width);
        this.scrollBody.css('margin-right', '0px').width(width);
        this.scrollFooter.width(width);

        var $this = this,
        headerWidth = width + this.frozenLayout.width();

        this.jq.children('.ui-widget-header').each(function() {
            $this.setOuterWidth($(this), headerWidth);
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    fixColumnWidths: function() {
        var $this = this;
        if(!this.columnWidthsFixed) {

            if(this.cfg.scrollable) {
                this._fixColumnWidths(this.scrollHeader, this.scrollFooterCols, this.scrollColgroup);
                this._fixColumnWidths(this.frozenHeader, this.frozenFooterCols, this.frozenColgroup);
            }
            else {
                this.jq.find('> .ui-datatable-tablewrapper > table > thead > tr > th').each(function() {
                    var col = $(this),
                    widthInfo = $this.getColumnWidthInfo(col);

                    $this.applyWidthInfo(col, widthInfo);
                });
            }

            this.columnWidthsFixed = true;
        }
    },

    /**
     * Adjusts the width of the given columns to fit the current settings.
     * @protected
     * @param {JQuery} header Header of this data table.
     * @param {JQuery} footerCols The columns to adjust.
     */
    _fixColumnWidths: function(header, footerCols) {
        var $this = this;

        header.find('> .ui-datatable-scrollable-header-box > table > thead > tr > th').each(function() {
            var headerCol = $(this),
            colIndex = headerCol.index(),
            widthInfo = $this.getColumnWidthInfo(headerCol);

            $this.applyWidthInfo(headerCol, widthInfo);

            if(footerCols.length > 0) {
                var footerCol = footerCols.eq(colIndex);
                $this.applyWidthInfo(footerCol, widthInfo);
            }
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {string} data
     * @param {boolean | undefined} clear
     */
    updateData: function(data, clear) {
        var table = $('<table><tbody>' + data + '</tbody></table>'),
        rows = table.find('> tbody > tr'),
        empty = (clear === undefined) ? true: clear;

        if(empty) {
            this.frozenTbody.children().remove();
            this.scrollTbody.children().remove();
        }

        //find slice index by checking how many rendered columns there are in frozen part
        var firstRow = this.frozenTbody.children('tr:first'),
        frozenColumnCount = firstRow.length ? firstRow.children('td').length: this.cfg.frozenColumns;

        for(var i = 0; i < rows.length; i++) {
            var row = rows.eq(i),
            columns = row.children('td'),
            frozenRow = this.copyRow(row),
            scrollableRow = this.copyRow(row);

            if(row.hasClass('ui-datatable-empty-message')) {
                var colspan = columns.attr('colspan'),
                cloneColumns = columns.clone();

                frozenRow.append(columns.attr('colspan', this.cfg.frozenColumns));
                scrollableRow.append(cloneColumns.attr('colspan', (colspan - this.cfg.frozenColumns)));
            }
            else {
                frozenRow.append(columns.slice(0, frozenColumnCount));
                scrollableRow.append(columns.slice(frozenColumnCount));
            }

            this.frozenTbody.append(frozenRow);
            this.scrollTbody.append(scrollableRow);
        }

        this.postUpdateData();
    },

    /**
     * Clones the given row and returns it
     * @param {JQuery} original DOM element of the original row.
     * @return {JQuery} The cloned row.
     */
    copyRow: function(original) {
        return $('<tr></tr>').attr('data-ri', original.data('ri')).attr('data-rk', original.data('rk')).addClass(original.attr('class')).
                attr('role', 'row').attr('aria-selected', original.attr('aria-selected'));
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getThead: function() {
        return $(this.jqId + '_frozenThead,' + this.jqId + '_scrollableThead');
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getTbody: function() {
        return $(this.jqId + '_frozenTbody,' + this.jqId + '_scrollableTbody');
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getTfoot: function() {
        return $(this.jqId + '_frozenTfoot,' + this.jqId + '_scrollableTfoot');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {string} selector
     */
    bindRowHover: function(selector) {
        var $this = this;

        this.tbody.off('mouseenter.datatable mouseleave.datatable', selector)
                    .on('mouseenter.datatable', selector, null, function() {
                        var row = $(this),
                        twinRow = $this.getTwinRow(row);

                        row.addClass('ui-state-hover');
                        twinRow.addClass('ui-state-hover');
                    })
                    .on('mouseleave.datatable', selector, null, function() {
                        var row = $(this),
                        twinRow = $this.getTwinRow(row);

                        row.removeClass('ui-state-hover');
                        twinRow.removeClass('ui-state-hover');
                    });
    },

    /**
     * Finds the twin row of the given row. The data table body has got two sets of rows.
     * @param {JQuery} row Row for which to find the twin
     * @return {JQuery} DOM element of the twin row.
     */
    getTwinRow: function(row) {
        var twinTbody = (this.tbody.index(row.parent()) === 0) ? this.tbody.eq(1) : this.tbody.eq(0);

        return twinTbody.children().eq(row.index());
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     */
    highlightRow: function(row) {
        this._super(row);
        this._super(this.getTwinRow(row));
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     */
    unhighlightRow: function(row) {
        this._super(row);
        this._super(this.getTwinRow(row));
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     * @param {string} content
     */
    displayExpandedRow: function(row, content) {
        var twinRow = this.getTwinRow(row);
        row.after(content);
        var expansionRow = row.next();
        this.updateColspan(expansionRow);
        expansionRow.show();

        twinRow.after('<tr class="ui-expanded-row-content ui-widget-content"><td></td></tr>');
        twinRow.next().children('td').attr('colspan', this.updateColspan(twinRow)).height(expansionRow.children('td').height());
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     */
    collapseRow: function(row) {
        this._super(row);
        this._super(this.getTwinRow(row));
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getExpandedRows: function() {
        return this.frozenTbody.children('.ui-expanded-row');
    },

    /**
     * @override
     * @inheritdoc
     * @protected
     * @param {JQuery} row
     */
    showRowEditors: function(row) {
        this._super(row);
        this._super(this.getTwinRow(row));
    },

    /**
     * @override
     * @inheritdoc
     * @protected
     * @param {JQuery} row
     * @param {string} content
     */
    updateRow: function(row, content) {
        var table = $('<table><tbody>' + content + '</tbody></table>'),
        newRow = table.find('> tbody > tr'),
        columns = newRow.children('td'),
        frozenRow = this.copyRow(newRow),
        scrollableRow = this.copyRow(newRow),
        twinRow = this.getTwinRow(row);

        frozenRow.append(columns.slice(0, this.cfg.frozenColumns));
        scrollableRow.append(columns.slice(this.cfg.frozenColumns));

        row.replaceWith(frozenRow);
        twinRow.replaceWith(scrollableRow);
    },

    /**
     * @override
     * @inheritdoc
     * @param {number} index
     */
    invalidateRow: function(index) {
        this.frozenTbody.children('tr').eq(index).addClass('ui-widget-content ui-row-editing ui-state-error');
        this.scrollTbody.children('tr').eq(index).addClass('ui-widget-content ui-row-editing ui-state-error');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     * @return {JQuery}
     */
    getRowEditors: function(row) {
        return row.find('div.ui-cell-editor').add(this.getTwinRow(row).find('div.ui-cell-editor'));
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQueryUI.DraggableEventUIParams} ui Data of the drag event.
     * @return {JQuery|null}
     */
    findGroupResizer: function(ui) {
        var resizer = this._findGroupResizer(ui, this.frozenGroupResizers);
        if(resizer) {
            return resizer;
        }
        else {
            return this._findGroupResizer(ui, this.scrollGroupResizers);
        }
    },

    /**
     * Finds the resizer DOM element that matches the given draggable event params.
     * @protected
     * @param {JQueryUI.DraggableEventUIParams} ui Data of the drag event.
     * @param {JQuery} resizers List of all available resizers.
     * @return {JQuery|null} DOM element of the resizer.
     */
    _findGroupResizer: function(ui, resizers) {
        for(var i = 0; i < resizers.length; i++) {
            var groupResizer = resizers.eq(i);
            if(groupResizer.offset().left === ui.helper.data('originalposition').left) {
                return groupResizer;
            }
        }

        return null;
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    addResizers: function() {
        var frozenColumns = this.frozenThead.find('> tr > th.ui-resizable-column'),
        scrollableColumns = this.scrollThead.find('> tr > th.ui-resizable-column');

        frozenColumns.prepend('<span class="ui-column-resizer">&nbsp;</span>');
        scrollableColumns.prepend('<span class="ui-column-resizer">&nbsp;</span>')

        if(this.cfg.resizeMode === 'fit') {
            frozenColumns.filter(':last-child').addClass('ui-frozen-column-last');
            scrollableColumns.filter(':last-child').children('span.ui-column-resizer').hide();
        }

        if(this.hasColumnGroup) {
            this.frozenGroupResizers = this.frozenThead.find('> tr:first > th > .ui-column-resizer');
            this.scrollGroupResizers = this.scrollThead.find('> tr:first > th > .ui-column-resizer');
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery.TriggeredEvent} event
     * @param {JQueryUI.DraggableEventUIParams} ui
     */
    resize: function(event, ui) {
        var columnHeader = null,
        change = null,
        newWidth = null,
        nextColumnWidth = null,
        expandMode = (this.cfg.resizeMode === 'expand');

        if(this.hasColumnGroup) {
            var groupResizer = this.findGroupResizer(ui);
            if(!groupResizer) {
                return;
            }

            columnHeader = groupResizer.parent();
        }
        else {
            columnHeader = ui.helper.parent();
        }

        var nextColumnHeader = columnHeader.next();

        var colIndex = columnHeader.index(),
        lastFrozen = columnHeader.hasClass('ui-frozen-column-last');

        if(this.cfg.liveResize) {
            change = columnHeader.outerWidth() - (event.pageX - columnHeader.offset().left),
            newWidth = (columnHeader.width() - change),
            nextColumnWidth = (nextColumnHeader.width() + change);
        }
        else {
            change = (ui.position.left - ui.originalPosition.left),
            newWidth = (columnHeader.width() + change),
            nextColumnWidth = (nextColumnHeader.width() - change);
        }

        var minWidth = parseInt(columnHeader.css('min-width'));
        minWidth = (minWidth == 0) ? 15 : minWidth;
        var shouldChange = (expandMode && newWidth > minWidth) || (lastFrozen ? (newWidth > minWidth) : (newWidth > minWidth && nextColumnWidth > minWidth));
        if(shouldChange) {
            var frozenColumn = columnHeader.hasClass('ui-frozen-column'),
            theadClone = frozenColumn ? this.frozenTheadClone : this.scrollTheadClone,
            originalTable = frozenColumn ? this.frozenThead.parent() : this.scrollThead.parent(),
            cloneTable = theadClone.parent(),
            footerCols = frozenColumn ? this.frozenFooterCols : this.scrollFooterCols,
            footerTable = frozenColumn ? this.frozenFooterTable:  this.scrollFooterTable,
            $this = this;

            if(expandMode) {
                if(lastFrozen) {
                    this.frozenLayout.width(this.frozenLayout.width() + change);
                }

                var originalTableWidth = originalTable.width(),
                cloneTableWidth = cloneTable.width(),
                footerTableWidth = footerTable.width();

                //header
                originalTable.width(originalTableWidth + change);

                //body
                cloneTable.width(cloneTableWidth + change);

                //footer
                footerTable.width(footerTableWidth + change);

                setTimeout(function() {
                    columnHeader.width(newWidth);

                    if($this.hasColumnGroup) {
                        theadClone.find('> tr:first').children('th').eq(colIndex).width(newWidth);                          //body
                        footerTable.find('> tfoot > tr:first').children('th').eq(colIndex).width(newWidth);                 //footer
                    }
                    else {
                        theadClone.find(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).width(newWidth);     //body
                        footerCols.eq(colIndex).width(newWidth);                                                            //footer
                    }
                }, 1);


            }
            else {
                if(lastFrozen) {
                    this.frozenLayout.width(this.frozenLayout.width() + change);
                }

                columnHeader.width(newWidth);
                nextColumnHeader.width(nextColumnWidth);

                if(this.hasColumnGroup) {
                    //body
                    theadClone.find('> tr:first').children('th').eq(colIndex).width(newWidth);
                    theadClone.find('> tr:first').children('th').eq(colIndex + 1).width(nextColumnWidth);

                    //footer
                    footerTable.find('> tfoot > tr:first').children('th').eq(colIndex).width(newWidth);
                    footerTable.find('> tfoot > tr:first').children('th').eq(colIndex + 1).width(nextColumnWidth);
                }
                else {
                    theadClone.find(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).width(newWidth);
                    theadClone.find(PrimeFaces.escapeClientId(nextColumnHeader.attr('id') + '_clone')).width(nextColumnWidth);

                    if(footerCols.length > 0) {
                        var footerCol = footerCols.eq(colIndex),
                        nextFooterCol = footerCol.next();

                        footerCol.width(newWidth);
                        nextFooterCol.width(nextColumnWidth);
                    }
                }
            }
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @return {boolean}
     */
    hasColGroup: function() {
        return this.frozenThead.children('tr').length > 1 || this.scrollThead.children('tr').length > 1;
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    addGhostRow: function() {
        this._addGhostRow(this.frozenTbody, this.frozenThead, this.frozenTheadClone, this.frozenFooter.find('table'), 'ui-frozen-column');
        this._addGhostRow(this.scrollTbody, this.scrollThead, this.scrollTheadClone, this.scrollFooterTable);
    },

    /**
     * Adds an invisible row for internal purposes.
     * @protected
     * @param {JQuery} body Body of this data table.
     * @param {JQuery} header Header of this data table.
     * @param {JQuery} headerClone Cloned header of this data table, see method `cloneHead`.
     * @param {JQuery} footerTable Footer of this data table.
     * @param {string} [columnClass] Optional CSS class for the ghost columns.
     */
    _addGhostRow: function(body, header, headerClone, footerTable, columnClass) {
        var dataColumns = body.find('tr:first').children('td'),
        dataColumnsCount = dataColumns.length,
        columnMarkup = '',
        columnStyleClass = columnClass ? 'ui-resizable-column ' + columnClass : 'ui-resizable-column';

        for(var i = 0; i < dataColumnsCount; i++) {
            columnMarkup += '<th style="height:0px;border-bottom-width: 0px;border-top-width: 0px;padding-top: 0px;padding-bottom: 0px;outline: 0 none;width:' + dataColumns.eq(i).width() + 'px" class="' + columnStyleClass + '"></th>';
        }

        header.prepend('<tr>' + columnMarkup + '</tr>');

        if(this.cfg.scrollable) {
            headerClone.prepend('<tr>' + columnMarkup + '</tr>');
            footerTable.children('tfoot').prepend('<tr>' + columnMarkup + '</tr>');
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @return {JQuery}
     */
    getFocusableTbody: function() {
        return this.tbody.eq(0);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    highlightFocusedRow: function() {
        this._super();
        this.getTwinRow(this.focusedRow).addClass('ui-state-hover');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    unhighlightFocusedRow: function() {
        this._super();
        this.getTwinRow(this.focusedRow).removeClass('ui-state-hover');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} row
     */
    assignFocusedRow: function(row) {
        this._super(row);

        if(!row.parent().attr('tabindex')) {
            this.frozenTbody.trigger('focus');
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    saveColumnOrder: function() {
        var columnIds = [],
        columns = $(this.jqId + '_frozenThead:first th,' + this.jqId + '_scrollableThead:first th');

        columns.each(function(i, item) {
            columnIds.push($(item).attr('id'));
        });

        this.orderStateHolder.val(columnIds.join(','));
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    resetVirtualScrollBody: function() {
        this.scrollBodyTable.css('top', '0px');
        this.frozenBodyTable.css('top', '0px');
        this.scrollBody.scrollTop(0);
        this.frozenBody.scrollTop(0);
        this.clearScrollState();
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    groupRows: function() {
        var scrollRows = this.scrollTbody.children('tr'),
        frozenRows = this.frozenTbody.children('tr');

        for (var i = 0; i < this.cfg.groupColumnIndexes.length; i++) {
            var groupColumnIndex = this.cfg.groupColumnIndexes[i];

            if (groupColumnIndex >= this.cfg.frozenColumns) {
                this.groupRow(groupColumnIndex - this.cfg.frozenColumns, scrollRows);
            }
            else {
                this.groupRow(groupColumnIndex, frozenRows);
            }
        }

        scrollRows.children('td.ui-duplicated-column').remove();
        frozenRows.children('td.ui-duplicated-column').remove();
    }
});

/**
 * __PrimeFaces Dialog Widget__
 * 
 * Dialog is a panel component that is displayed as an overlay on top of other elements on the current page. Optionally,
 * the dialog may be modal and block the user from interacting with elements below the dialog.
 *
 * @typedef PrimeFaces.widget.Dialog.OnHideCallback Client-side callback to invoke when the dialog is closed, see
 * {@link DialogCfg.onHide}.
 * @this {PrimeFaces.widget.Dialog} PrimeFaces.widget.Dialog.OnHideCallback
 * 
 * @typedef PrimeFaces.widget.Dialog.OnShowCallback Client-side callback to invoke when the dialog is opened, see
 * {@link DialogCfg.onShow}
 * @this {PrimeFaces.widget.Dialog} PrimeFaces.widget.Dialog.OnShowCallback
 * 
 * @prop {JQuery} closeIcon DOM element of the icon for closing this dialog, when this dialog is closable (an `x` by
 * default).
 * @prop {JQuery} content DOM element of the container for the content of this dialog.
 * @prop {JQuery} footer DOM element of the container with the footer of this dialog.
 * @prop {JQuery} icons DOM elements of the title bar icons of this dialog.
 * @prop {HTMLElement} jqEl The native DOM element instance of the container element of this widget (same element as the
 * `jq` property).
 * @prop {JQuery} maximizeIcon DOM element of the icon for maximizing this dialog, when this dialog can be maximized.
 * @prop {JQuery} minimizeIcon DOM element of the icon for minimizing this dialog, when this dialog can be minimized.
 * @prop {JQuery} titlebar DOM element of the title bar container of this dialog.
 * 
 * @interface {PrimeFaces.widget.DialogCfg} cfg The configuration for the {@link  Dialog| Dialog 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.DynamicOverlayWidgetCfg} cfg
 * @prop {string} cfg.appendTo A search expression for the element to which the dialog is appended. Defaults to the
 * body.
 * @prop {boolean} cfg.absolutePositioned Whether the dialog is positioned absolutely.
 * @prop {boolean} cfg.blockScroll Whether to prevent the document from scrolling when the dialog is visible.
 * @prop {boolean} cfg.cache Only relevant for dynamic="true": Defines if activating the dialog should load the contents from server again. For cache="true" (default) the dialog content is only loaded once..
 * @prop {boolean} cfg.closeOnEscape Whether the dialog is closed when the user presses the escape button.
 * @prop {boolean} cfg.closable Whether the dialog can be closed by the user.
 * @prop {boolean} cfg.draggable Whether the dialog is draggable.
 * @prop {boolean} cfg.dynamic Whether lazy loading of the content via AJAX is enabled.
 * @prop {boolean} cfg.fitViewport Dialog size might exceed the viewport if the content is taller than viewport in terms
 * of height. When this is set to `true`, automatically adjust the height to fit the dialog within the viewport.
 * @prop {number} cfg.height The height of the dialog in pixels.
 * @prop {string} cfg.hideEffect Effect to use when hiding the dialog.
 * @prop {string} cfg.iframeTitle The title of the iframe with the dialog.
 * @prop {boolean} cfg.maximizable Whether the dialog is maximizable.
 * @prop {number} cfg.minHeight The minimum height of the dialog in pixels.
 * @prop {boolean} cfg.minimizable Whether the dialog is minimizable.
 * @prop {number} cfg.minWidth The minimum width of the dialog in pixels.
 * @prop {boolean} cfg.modal Whether the dialog is modal and blocks the main content and other dialogs.
 * @prop {string} cfg.my Position of the dialog relative to the target.
 * @prop {PrimeFaces.widget.Dialog.OnHideCallback} cfg.onHide Client-side callback to invoke when the dialog is
 * closed.
 * @prop {PrimeFaces.widget.Dialog.OnShowCallback} cfg.onShow Client-side callback to invoke when the dialog is opened.
 * @prop {string} cfg.position Defines where the dialog should be displayed
 * @prop {boolean} cfg.resizable Whether the dialog can be resized by the user.
 * @prop {boolean} cfg.responsive Whether the dialog is responsive. In responsive mode, the dialog adjusts itself based
 * on the screen width.
 * @prop {string} cfg.showEffect Effect to use when showing the dialog
 * @prop {string} cfg.styleClass One or more CSS classes for the dialog.
 * @prop {number} cfg.width The width of the dialog in pixels.
 */
PrimeFaces.widget.Dialog = PrimeFaces.widget.DynamicOverlayWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.content = this.jq.children('.ui-dialog-content');
        this.titlebar = this.jq.children('.ui-dialog-titlebar');
        this.footer = this.jq.find('.ui-dialog-footer');
        this.icons = this.titlebar.children('.ui-dialog-titlebar-icon');
        this.closeIcon = this.titlebar.children('.ui-dialog-titlebar-close');
        this.minimizeIcon = this.titlebar.children('.ui-dialog-titlebar-minimize');
        this.maximizeIcon = this.titlebar.children('.ui-dialog-titlebar-maximize');
        this.cfg.absolutePositioned = this.jq.hasClass('ui-dialog-absolute');
        this.jqEl = this.jq[0];

        this.positionInitialized = false;

        //configuration
        this.cfg.width = this.cfg.width||'auto';
        this.cfg.height = this.cfg.height||'auto';
        this.cfg.draggable = this.cfg.draggable === false ? false : true;
        this.cfg.resizable = this.cfg.resizable === false ? false : true;
        this.cfg.minWidth = this.cfg.minWidth||150;
        this.cfg.minHeight = this.cfg.minHeight||this.titlebar.outerHeight();
        this.cfg.my = this.cfg.my||'center';
        this.cfg.position = this.cfg.position||'center';
        this.cfg.cache = this.cfg.cache === false ? false : true;
        this.parent = this.jq.parent();

        this.initSize();
        
        //events
        this.bindEvents();

        if(this.cfg.draggable) {
            this.setupDraggable();
        }

        if(this.cfg.resizable){
            this.setupResizable();
        }

        //docking zone
        if($(document.body).children('.ui-dialog-docking-zone').length === 0) {
            $(document.body).append('<div class="ui-dialog-docking-zone"></div>');
        }

        //aria
        this.applyARIA();

        if(this.cfg.visible){
            this.show();
        }

        if(this.cfg.responsive) {
            this.bindResizeListener();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.positionInitialized = false;
        this.loaded = false;

        $(document).off('keydown.dialog_' + cfg.id);

        if(this.minimized) {
            var dockingZone = $(document.body).children('.ui-dialog-docking-zone');
            if(dockingZone.length && dockingZone.children(this.jqId).length) {
                this.removeMinimize();
                dockingZone.children(this.jqId).remove();
            }
        }

        this.minimized = false;
        this.maximized = false;

        this._super(cfg);
    },

    /**
     * Computes and applies the correct size for this dialog, according to the current configuration.
     * @protected
     */
    initSize: function() {
        this.jq.css({
            'width': String(this.cfg.width),
            'height': 'auto'
        });

        this.content.height(this.cfg.height);

        if(this.cfg.fitViewport) {
            this.fitViewport();
        }
    },

    /**
     * Makes this dialog fit the current browser window, if the `fitViewport` option is enabled.
     * @protected
     */
    fitViewport: function() {
        var windowHeight = $(window).height();

        var margin = this.jq.outerHeight(true) - this.jq.outerHeight();
        var headerHeight = this.titlebar.outerHeight(true);
        var contentPadding = this.content.innerHeight() - this.content.height();
        var footerHeight = this.footer.outerHeight(true) || 0;

        var maxHeight = windowHeight - (margin + headerHeight + contentPadding + footerHeight);

        this.content.css('max-height', String(maxHeight));
    },


    /**
     * @override
     * @protected
     * @inheritdoc
     * @return {JQuery} The DOM elements which are allowed to be focused via tabbing.
     */
    getModalTabbables: function(){
        return this.jq.find(':tabbable').add(this.footer.find(':tabbable'));
    },

    /**
     * Displays this dialog. In case the `dynamic` option is enabled and the content was not yet loaded, this may result
     * in an AJAX request to the sever to retrieve the content. Also triggers the show behaviors registered for this
     * dialog.
     * 
     * @param {number | string} [duration] Durations are given in milliseconds; higher values indicate slower
     * animations, not faster ones. The strings `fast` and `slow` can be supplied to indicate durations of 200 and 600
     * milliseconds, respectively.
     */
    show: function(duration) {
        if(this.isVisible()) {
            return;
        }

        if(!this.loaded && this.cfg.dynamic) {
            this.loadContents();
        }
        else {
            if (this.positionInitialized === false) {
                this.jqEl.style.visibility = "hidden";
                this.jqEl.style.display = "block";
                this.initPosition();
                this.jqEl.style.display = "none";
                this.jqEl.style.visibility = "visible";
            }

            this._show(duration);

            if(this.cfg.dynamic && !this.cfg.cache) {
                this.loaded = false;
            }
        }
    },

    /**
     * Performs the client-side actions needed to actually show this dialog. Compare to `show`, which loads the dialog
     * content from the server if required, then call this method.
     * 
     * @protected
     * 
     * @param {number | string} [duration] Durations are given in milliseconds; higher values indicate slower
     * animations, not faster ones. The strings `fast` and `slow` can be supplied to indicate durations of 200 and 600
     * milliseconds, respectively.
     */
    _show: function(duration) {
        this.moveToTop();

        //offset
        if(this.cfg.absolutePositioned) {
            var winScrollTop = $(window).scrollTop();
            this.jq.css('top', String(parseFloat(this.jq.css('top')) + (winScrollTop - this.lastScrollTop)));
            this.lastScrollTop = winScrollTop;
        }

        var animated = this.cfg.showEffect;
        if(animated) {
            var $this = this;

            this.jq.show(this.cfg.showEffect, duration, 'normal', function() {
                $this.postShow();
            });
        }
        else {
            //display dialog
            this.jq.show(duration);

            this.postShow();
        }

        if(this.cfg.modal) {
            this.enableModality();
        }
    },

    /**
     * Called after this dialog became visible. Triggers the behaviors and registered event listeners.
     * @protected
     */
    postShow: function() {
        if (this.cfg.fitViewport) {
            this.fitViewport();
        }
        
        this.callBehavior('open');

        PrimeFaces.invokeDeferredRenders(this.id);

        //execute user defined callback
        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }

        this.jq.attr({
            'aria-hidden': false
            ,'aria-live': 'polite'
        });

        this.applyFocus();
    },

    /**
     * Hide the dialog with an optional animation lasting for the given duration.
     * 
     * @param {number | string} [duration] Durations are given in milliseconds; higher values indicate slower
     * animations, not faster ones. The strings `fast` and `slow` can be supplied to indicate durations of 200 and 600
     * milliseconds, respectively.
     */
    hide: function(duration) {
        if(!this.isVisible()) {
            return;
        }

        var animated = this.cfg.hideEffect;
        if(animated) {
            var $this = this;

            this.jq.hide(this.cfg.hideEffect, duration, 'normal', function() {
                if($this.cfg.modal) {
                    $this.disableModality();
                }
                $this.onHide();
            });
        }
        else {
            this.jq.hide();
            if(this.cfg.modal) {
                this.disableModality();
            }
            this.onHide(duration);
        }
    },

    /**
     * Puts focus on the first element that can be focused.
     * @protected
     */
    applyFocus: function() {
        if(this.cfg.focus)
        	PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.focus).trigger('focus');
        else
            PrimeFaces.focus(null, this.id);
    },

    /**
     * Sets up all event listeners required by this widget.
     * @protected
     */
    bindEvents: function() {
        var $this = this;

        //Move dialog to top if target is not a trigger for a PrimeFaces overlay
        this.jq.on("mousedown", function(e) {
            if(!$(e.target).data('primefaces-overlay-target')) {
                $this.moveToTop();
            }
        });

        this.icons.on('mouseover', function() {
            $(this).addClass('ui-state-hover');
        }).on('mouseout', function() {
            $(this).removeClass('ui-state-hover');
        }).on('focus', function() {
            $(this).addClass('ui-state-focus');
        }).on('blur', function() {
            $(this).removeClass('ui-state-focus');
        });

        this.closeIcon.on('click', function(e) {
            $this.hide();
            e.preventDefault();
        });

        this.maximizeIcon.on("click", function(e) {
            $this.toggleMaximize();
            e.preventDefault();
        });

        this.minimizeIcon.on("click", function(e) {
            $this.toggleMinimize();
            e.preventDefault();
        });

        if(this.cfg.closeOnEscape) {
            $(document).on('keydown.dialog_' + this.id, function(e) {
                var keyCode = $.ui.keyCode;
                if(e.which === keyCode.ESCAPE && $this.isVisible()) {
                    // GitHub #6677 if multiple dialogs check if this is the topmost active dialog to close
                    var active = parseInt($this.jq.css('z-index')) === parseInt($('.ui-dialog:visible').last().css('z-index'));
                    if(active) {
                         $this.hide();
                    }
                };
            });
        }
    },

    /**
     * Sets up all event listeners required to make this dialog draggable.
     * @protected
     */
    setupDraggable: function() {
        var $this = this;

        this.jq.draggable({
            cancel: '.ui-dialog-content, .ui-dialog-titlebar-close',
            handle: '.ui-dialog-titlebar',
            containment : $this.cfg.absolutePositioned ? 'document' : 'window',
            stop: function( event, ui ) {
                if($this.hasBehavior('move')) {
                    var ext = {
                        params: [
                            {name: $this.id + '_top', value: ui.offset.top},
                            {name: $this.id + '_left', value: ui.offset.left}
                        ]
                    };
                    $this.callBehavior('move', ext);
                }
            }
        });
    },

    /**
     * Sets up all event listeners required to make this dialog resizable.
     * @protected
     */
    setupResizable: function() {
        var $this = this;

        this.jq.resizable({
            handles : 'n,s,e,w,ne,nw,se,sw',
            minWidth : this.cfg.minWidth,
            minHeight : this.cfg.minHeight,
            alsoResize : this.content,
            containment: 'document',
            start: function(event, ui) {
                $this.jq.data('offset', $this.jq.offset());

                if($this.cfg.hasIframe) {
                    $this.iframeFix = $('<div style="position:absolute;background-color:transparent;width:100%;height:100%;top:0;left:0;"></div>').appendTo($this.content);
                }

                if ($this.hasBehavior('resizeStart')) {
                    var ext = {
                        params: [
                            {name: $this.id + '_width', value: ui.size.width},
                            {name: $this.id + '_height', value: ui.size.height}
                        ]
                    };
                    $this.callBehavior('resizeStart', ext);
                }
            },
            stop: function(event, ui) {
                $this.jq.css('position', 'fixed');

                if($this.cfg.hasIframe) {
                    $this.iframeFix.remove();
                }

                if ($this.hasBehavior('resizeStop')) {
                    var ext = {
                        params: [
                            {name: $this.id + '_width', value: ui.size.width},
                            {name: $this.id + '_height', value: ui.size.height}
                        ]
                    };
                    $this.callBehavior('resizeStop', ext);
                }
            }
        });

        this.resizers = this.jq.children('.ui-resizable-handle');
    },
    
    /**
     * Resets the dialog position as specified by the `position` property of this widget configuration.
     * @protected
     */
    resetPosition: function() {
       this.initPosition();
    },

    /**
     * Positions this dialog on the screen as specified by the widget configuration.
     * @protected
     */
    initPosition: function() {
        var $this = this;

        //reset
        this.jq.css({left:'0',top:'0'});

        if(/(center|left|top|right|bottom)/.test(this.cfg.position)) {
            this.cfg.position = this.cfg.position.replace(',', ' ');

            this.jq.position({
                        my: this.cfg.my
                        ,at: this.cfg.position
                        ,collision: 'fit'
                        ,of: window
                        //make sure dialog stays in viewport
                        ,using: function(pos) {
                            var l = pos.left < 0 ? 0 : pos.left,
                            t = pos.top < 0 ? 0 : pos.top,
                            scrollTop = $(window).scrollTop();

                            //offset
                            if($this.cfg.absolutePositioned) {
                                t += scrollTop;
                                $this.lastScrollTop = scrollTop;
                            }

                            $(this).css({
                                left: l + 'px'
                                ,top: t + 'px'
                            });
                        }
                    });
        }
        else {
            var coords = this.cfg.position.split(','),
            x = PrimeFaces.trim(coords[0]),
            y = PrimeFaces.trim(coords[1]);

            this.jq.offset({
                left: x
                ,top: y
            });
        }

        this.positionInitialized = true;
    },

    /**
     * Called when this dialog was closed. Invokes the appropriate behaviors and event listeners.
     * @protected
     * @param {unknown} [event] Unused.
     * @param {unknown} [ui] Unused. 
     */
    onHide: function(event, ui) {
        this.callBehavior('close');

        this.jq.attr({
            'aria-hidden': true
            ,'aria-live': 'off'
        });

        if(this.cfg.onHide) {
            this.cfg.onHide.call(this, event, ui);
        }
    },

    /**
     * Moves this dialog to the top so that it is positioned above other elements and overlays.
     */
    moveToTop: function() {
        this.jq.css('z-index', PrimeFaces.nextZindex());
    },

    /**
     * Toggle maxification, as if the user had clicked the maximize button. If this dialog is not yet maximized,
     * maximizes it. If this dialog is already maximized, reverts it back to its orignal size.
     */
    toggleMaximize: function() {
        if(this.minimized) {
            this.toggleMinimize();
        }

        if(this.maximized) {
            this.jq.removeClass('ui-dialog-maximized');
            this.restoreState();

            this.maximizeIcon.children('.ui-icon').removeClass('ui-icon-newwin').addClass('ui-icon-extlink');
            this.maximized = false;

            this.callBehavior('restoreMaximize');
        }
        else {
            this.saveState();

            var win = $(window);

            this.jq.addClass('ui-dialog-maximized').css({
                'width': String(win.width() - 6)
                ,'height': String(win.height())
            }).offset({
                top: win.scrollTop()
                ,left: win.scrollLeft()
            });

            //maximize content
            var contentPadding = this.content.innerHeight() - this.content.height();
            this.content.css({
                width: 'auto',
                height: String(this.jq.height() - this.titlebar.outerHeight() - contentPadding)
            });

            this.maximizeIcon.removeClass('ui-state-hover').children('.ui-icon').removeClass('ui-icon-extlink').addClass('ui-icon-newwin');
            this.maximized = true;

            this.callBehavior('maximize');
        }
    },

    /**
     * Toggles minification, as if the user had clicked the minimize button. If this dialog is not yet minimized,
     * minimizes it.  If this dialog is already minimized, restores its original position.
     */
    toggleMinimize: function() {
        var animate = true,
        dockingZone = $(document.body).children('.ui-dialog-docking-zone');

        if(this.maximized) {
            this.toggleMaximize();
            animate = false;
        }

        var $this = this;

        if(this.minimized) {
            this.removeMinimize();

            this.callBehavior('restoreMinimize');
        }
        else {
            this.saveState();

            if(animate) {
                this.jq.effect('transfer', {
                                to: dockingZone
                                ,className: 'ui-dialog-minimizing'
                                }, 500,
                                function() {
                                    $this.dock(dockingZone);
                                    $this.jq.addClass('ui-dialog-minimized');
                                });
            }
            else {
                this.dock(dockingZone);
                this.jq.addClass('ui-dialog-minimized');
            }
        }
    },

    /**
     * Docks this dialog to the given docking zone. The docking zone is usually at the bottom of the screen and displays
     * a list of minimized dialogs.
     * @protected
     * @param {JQuery} zone Zone to dock to.
     */
    dock: function(zone) {
        zone.css('z-index', this.jq.css('z-index'));
        this.jq.appendTo(zone).css('position', 'static');
        this.jq.css({'height':'auto', 'width':'auto', 'float': 'left'});
        this.content.hide();
        this.footer.hide();
        this.minimizeIcon.removeClass('ui-state-hover').children('.ui-icon').removeClass('ui-icon-minus').addClass('ui-icon-plus');
        this.minimized = true;

        if(this.cfg.resizable) {
            this.resizers.hide();
        }

        this.callBehavior('minimize');
    },

    /**
     * Saves the current state of this dialog, such as its width and height. Used for example to preserve that state
     * during AJAX updates.
     * @protected
     */
    saveState: function() {
        this.state = {
            width: this.jq.width(),
            height: this.jq.height(),
            contentWidth: this.content.width(),
            contentHeight: this.content.height()
        };

        var win = $(window);
        this.state.offset = this.jq.offset();
        this.state.windowScrollLeft = win.scrollLeft();
        this.state.windowScrollTop = win.scrollTop();
    },

    /**
     * Restores the state as saved by `saveState`, usually called after an AJAX update.
     * @protected
     */
    restoreState: function() {
        this.jq.width(this.state.width).height(this.state.height);
        this.content.width(this.state.contentWidth).height(this.state.contentHeight);

        var win = $(window);
        this.jq.offset({
                top: this.state.offset.top + (win.scrollTop() - this.state.windowScrollTop)
                ,left: this.state.offset.left + (win.scrollLeft() - this.state.windowScrollLeft)
        });
    },

    /**
     * Loads the content of the dialog via AJAx, if this dialog is `dynamic` and the the content has not yet been
     * loaded.
     * @protected
     */
    loadContents: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.loaded = true;
                $this.show();
            }
        };

        if(this.hasBehavior('loadContent')) {
            this.callBehavior('loadContent', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Applies all `ARIA` attributes to the contents of this dialog.
     * @protected
     */
    applyARIA: function() {
        this.jq.attr({
            'role': 'dialog'
            ,'aria-describedby': this.id + '_content'
            ,'aria-hidden': !this.cfg.visible
            ,'aria-modal': this.cfg.modal
        });
        
        // GitHub #4727
        var title = this.id + '_title';
        if ($(PrimeFaces.escapeClientId(title)).length) {
            this.jq.attr('aria-labelledby', title);
        }

        this.titlebar.children('a.ui-dialog-titlebar-icon').attr('role', 'button');
    },

    /**
     * Checks whether this dialog is opened and visible. This method returns `true` irrespective of whether this dialog 
     * is minimized, maximized, or shown normally. Returns `false` only when this dialog is closed. 
     * @return {boolean} `true` if this dialog is currently being shown, `false` otherwise.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },

    /**
     * Sets up the event listeners for handling resize events.
     * @protected
     */
    bindResizeListener: function() {
        var $this = this;

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', null, function() {
            if ($this.cfg.fitViewport) {
                $this.fitViewport();
            }

            if ($this.isVisible()) {
                // instant reinit position
                $this.initPosition();
            }
            else {
                // reset, so the dialog will be positioned again when showing the dialog next time
                $this.positionInitialized = false;
            }
        });
        PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_align', function() {
            if ($this.isVisible()) {
                // instant reinit position
                $this.initPosition();
            }
            else {
                // reset, so the dialog will be positioned again when showing the dialog next time
                $this.positionInitialized = false;
            }
        });
    },

    /**
     * Called when this dialog is minimized. Restores the original position of this dialog.
     * @protected
     */
    removeMinimize: function() {
        this.jq.appendTo(this.parent).removeClass('ui-dialog-minimized').css({'position':'fixed', 'float':'none'});
        this.restoreState();
        this.content.show();
        this.footer.show();
        this.minimizeIcon.removeClass('ui-state-hover').children('.ui-icon').removeClass('ui-icon-plus').addClass('ui-icon-minus');
        this.minimized = false;

        if(this.cfg.resizable) {
            this.resizers.show();
        }
    }

});

/**
 * __PrimeFaces ConfirmDialog Widget__
 * 
 * ConfirmDialog is a replacement to the legacy JavaScript confirmation box. Skinning, customization and avoiding popup
 * blockers are notable advantages over the classic JavaScript confirmation box.
 * 
 * @interface {PrimeFaces.widget.ConfirmDialog.ConfirmDialogMessage} ConfirmDialogMessage Interface for the message that
 * is shown in the confirm dialog.
 * @prop {string} ConfirmDialogMessage.header Header of the dialog message.
 * @prop {string} ConfirmDialogMessage.message Main content of the dialog message.
 * @prop {boolean} ConfirmDialogMessage.escape If `true`, the message is escaped for HTML. If `false`, the message is
 * interpreted as an HTML string.
 * @prop {string} [ConfirmDialogMessage.icon] Optional icon that is shown to the left of the confirm dialog. When not given, defaults to
 * `ui-icon-alert`. Must be a style class of some icon font.
 * @prop {string} [ConfirmDialogMessage.beforeShow] Optional code that is run before the message is shown. Must be valid JavaScript code.
 * It is evaluated via {@link PrimeFaces.csp.eval}.
 * 
 * @prop {JQuery} title DOM element of the title bar text.
 * @prop {JQuery} message DOM element of the confirmation message displayed in this confirm dialog.
 * @prop {JQuery} icon DOM element of the icon displayed next to the confirmation message.
 * 
 * @interface {PrimeFaces.widget.ConfirmDialogCfg} cfg The configuration for the
 * {@link  ConfirmDialog| ConfirmDialog 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.DialogCfg} cfg
 */
PrimeFaces.widget.ConfirmDialog = PrimeFaces.widget.Dialog.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        cfg.draggable = false;
        cfg.resizable = false;
        cfg.modal = true;

        if (!cfg.appendTo && cfg.global) {
        	cfg.appendTo = '@(body)';
        }

        this._super(cfg);

        this.title = this.titlebar.children('.ui-dialog-title');
        this.message = this.content.children('.ui-confirm-dialog-message');
        this.icon = this.content.children('.ui-confirm-dialog-severity');

        if(this.cfg.global) {
            PrimeFaces.confirmDialog = this;

            this.jq.on('click.ui-confirmdialog', '.ui-confirmdialog-yes, .ui-confirmdialog-no', null, function(e) {
                var el = $(this);

                if(el.hasClass('ui-confirmdialog-yes') && PrimeFaces.confirmSource) {
                    var id = PrimeFaces.confirmSource.get(0);
                    var js = PrimeFaces.confirmSource.data('pfconfirmcommand');

                    PrimeFaces.csp.executeEvent(id, js, e);

                    PrimeFaces.confirmDialog.hide();
                    PrimeFaces.confirmSource = null;
                }
                else if(el.hasClass('ui-confirmdialog-no')) {
                    PrimeFaces.confirmDialog.hide();
                    PrimeFaces.confirmSource = null;
                }

                e.preventDefault();
            });
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    applyFocus: function() {
        this.jq.find(':button,:submit').filter(':visible:enabled').eq(0).trigger('focus');
    },

    /**
     * Shows the given message in this confirmation dialog.
     * @param {Partial<PrimeFaces.widget.ConfirmDialog.ConfirmDialogMessage>} msg Message to show.
     */
    showMessage: function(msg) {
        if(msg.beforeShow) {
            PrimeFaces.csp.eval(msg.beforeShow);
        }

        var icon = (msg.icon === 'null') ? 'ui-icon-alert' : msg.icon;
        this.icon.removeClass().addClass('ui-icon ui-confirm-dialog-severity ' + icon);

        if(msg.header)
            this.title.text(msg.header);

        if(msg.message){
            if (msg.escape){
                this.message.text(msg.message);
            }
            else {
            	this.message.html(msg.message);
            }
        }

        this.show();
    }

});

/**
 * __PrimeFaces Dynamic Dialog Widget__ 
 * 
 * Used by the dialog framework for displaying other JSF views or external pages in a dialog on the current.
 * 
 * @interface {PrimeFaces.widget.DynamicDialogCfg} cfg The configuration for the {@link  DynamicDialog| DynamicDialog 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.DialogCfg} cfg
 */
PrimeFaces.widget.DynamicDialog = PrimeFaces.widget.Dialog.extend({

    /**
     * @override
     * @inheritdoc
     */
    show: function() {
        if(this.jq.hasClass('ui-overlay-visible')) {
            return;
        }

        if(this.positionInitialized === false) {
            this.initPosition();
        }

        this._show();
    },


    /**
     * @override
     * @protected
     * @inheritdoc
     */
    _show: function() {
        //replace visibility hidden with display none for effect support, toggle marker class
        this.jq.removeClass('ui-overlay-hidden').addClass('ui-overlay-visible').css({
            'display':'none'
            ,'visibility':'visible'
        });

        this.moveToTop();

        this.jq.show();

        if(this.cfg.height != "auto") {
            this.content.height(this.jq.outerHeight() - this.titlebar.outerHeight(true));
        }

        this.postShow();

        if(this.cfg.modal) {
            this.enableModality();
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    initSize: function() {
        this.jq.css({
            'width': String(this.cfg.width),
            'height': String(this.cfg.height)
        });

        if(this.cfg.fitViewport) {
            this.fitViewport();
        }
    }

});

/**
 * __PrimeFaces Draggable Widget__
 * 
 * Drag&Drop utilities of PrimeFaces consists of two components; Draggable and Droppable.
 * 
 * @typedef PrimeFaces.widget.Draggable.OnStartCallback Callback for when dragging starts. See also {@link
 * DraggableCfg.onStart}.
 * @this {PrimeFaces.widget.Draggable} PrimeFaces.widget.Draggable.OnStartCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Draggable.OnStartCallback.event The drag event that occurred.
 * @param {JQueryUI.DraggableEventUIParams} PrimeFaces.widget.Draggable.OnStartCallback.ui Details about the drag event.
 * 
 * @typedef PrimeFaces.widget.Draggable.OnStopCallback Callback for when dragging ends. See also {@link
 * DraggableCfg.onStop}.
 * @this {PrimeFaces.widget.Draggable} PrimeFaces.widget.Draggable.OnStopCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Draggable.OnStopCallback.event The drag event that occurred.
 * @param {JQueryUI.DraggableEventUIParams} PrimeFaces.widget.Draggable.OnStopCallback.ui Details about the drag event.
 * 
 * @interface {PrimeFaces.widget.DraggableCfg} cfg The configuration for the {@link  Draggable| Draggable 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
 * @extends {JQueryUI.DraggableOptions} cfg
 * 
 * @prop {PrimeFaces.widget.Draggable.OnStartCallback} cfg.onStart Callback for when dragging starts.
 * @prop {PrimeFaces.widget.Draggable.OnStopCallback} cfg.onStop Callback for when dragging ends.
 * @prop {string} cfg.target ID of the target of this draggable.
 */
PrimeFaces.widget.Draggable = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.jqTarget = $(PrimeFaces.escapeClientId(this.cfg.target));
        this.cfg.cancel = this.cfg.cancel || "input,textarea,button,select,option";

        if(this.cfg.appendTo) {
            this.cfg.appendTo = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.appendTo);
        }
        
        var $this = this;

        this.cfg.start = function(event, ui) {
            if($this.cfg.onStart) {
                $this.cfg.onStart.call($this, event, ui);
            }
        };
        
        this.cfg.stop = function(event, ui) {
            if($this.cfg.onStop) {
                $this.cfg.onStop.call($this, event, ui);
            }
        };
        
        this.jqTarget.draggable(this.cfg);
    }
    
});

/**
 * __PrimeFaces Droppable Widget__
 * 
 * Drag&Drop utilities of PrimeFaces consists of two components; Draggable and Droppable.
 * 
 * @typedef PrimeFaces.widget.Droppable.OnDropCallback Callback for when an items is dropped. See also {@link
 * Droppable.onDrop}.
 * @this {PrimeFaces.widget.Droppable} PrimeFaces.widget.Droppable.OnDropCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Droppable.OnDropCallback.event The drop event that occurred.
 * @param {JQueryUI.DroppableEventUIParam} PrimeFaces.widget.Droppable.OnDropCallback.ui Details about the drop event.
 * 
 * @interface {PrimeFaces.widget.DroppableCfg} cfg The configuration for the {@link  Droppable| Droppable 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
 * @extends {JQueryUI.DroppableOptions} cfg
 * 
 * @prop {PrimeFaces.widget.Droppable.OnDropCallback} onDrop Callback for when an items is dropped.
 * @prop {string} cfg.target ID of the target of this droppable.
 */
PrimeFaces.widget.Droppable = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.jqTarget = $(PrimeFaces.escapeClientId(this.cfg.target));

        this.bindDropListener();

        this.jqTarget.droppable(this.cfg);
    },

    /**
     * Sets up the vent listener for when an item is dropped.
     * @private
     */
    bindDropListener: function() {
        var _self = this;

        this.cfg.drop = function(event, ui) {
            if(_self.cfg.onDrop) {
                _self.cfg.onDrop.call(_self, event, ui);
            }
            if(_self.cfg.behaviors) {
                var dropBehavior = _self.cfg.behaviors['drop'];

                if(dropBehavior) {
                    var ext = {
                        params: [
                            {name: _self.id + '_dragId', value: ui.draggable.attr('id')},
                            {name: _self.id + '_dropId', value: _self.cfg.target}
                        ]
                    };

                    dropBehavior.call(_self, ext);
                }
            }
        };
    }
    
});
/**
 * __PrimeFaces Effect Widget__
 * 
 * Effect component is based on the jQuery UI effects library.
 * 
 * @typedef {() => void} PrimeFaces.widget.Effect.EffectCallback Function that run the effect when invoked. See also
 * {@link EffectCfg.fn}.
 * 
 * @prop {JQuery} source The DOM element with the source for the effect.
 * @prop {number} timeoutId ID of the current `setTimeout` for scheduling the effect.
 * 
 * @interface {PrimeFaces.widget.EffectCfg} cfg The configuration for the {@link  Effect| Effect 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
 * @extends {JQueryUI.EffectOptions} cfg
 * 
 * @prop {string} cfg.source ID of the source element for the effect.
 * @prop {number} cfg.delay Delay between effect repetitions in milliseconds.
 * @prop {string} cfg.event Event that triggers the effect. Defaults to `load` (page load).
 * @prop {PrimeFaces.widget.Effect.EffectCallback} cfg.fn Function that runs the effect when invoked.
 */
PrimeFaces.widget.Effect = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.source = $(PrimeFaces.escapeClientId(this.cfg.source));
        var _self = this;

        this.runner = function() {
            //avoid queuing multiple runs
            if(_self.timeoutId) {
                clearTimeout(_self.timeoutId);
            }

            _self.timeoutId = setTimeout(_self.cfg.fn, _self.cfg.delay);
        };

        if(this.cfg.event == 'load') {
            this.runner.call();
        } 
        else {
            this.source.on(this.cfg.event, this.runner);
        }
    }
    
});
/**
 * __PrimeFaces Fieldset Widget__
 * 
 * Fieldset is a grouping component as an extension to html fieldset.
 * 
 * @prop {JQuery} legend The DOM element with the legend of this fieldset.
 * @prop {JQuery} toggler The DOM element with the toggler for hiding or showing the content of this fieldset.
 * @prop {JQuery} content The DOM element with the content of this fieldset.
 * @prop {JQuery} stateHolder The DOM element with the hidden input field for the state of this fieldset.
 * @prop {boolean} loaded When dynamic loading is enabled, whether the content was already loaded.
 * 
 * @interface {PrimeFaces.widget.FieldsetCfg} cfg The configuration for the {@link  Fieldset| Fieldset 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.toggleable Whether the content of this fieldset can be toggled between expanded and collapsed.
 * @prop {boolean} cfg.collapsed Whether this fieldset is currently collapsed (content invisible) or expanded (content
 * visible).
 * @prop {number} cfg.toggleSpeed Toggle duration in milliseconds.
 * @prop {boolean} cfg.dynamic `true` to load the content via AJAX when the fieldset panel is opened, `false` to load
 * the content immediately.
 */
PrimeFaces.widget.Fieldset = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.legend = this.jq.children('.ui-fieldset-legend');

        var $this = this;

        if(this.cfg.toggleable) {
            this.content = this.jq.children('.ui-fieldset-content');
            this.toggler = this.legend.children('.ui-fieldset-toggler');
            this.stateHolder = $(this.jqId + '_collapsed');

            //Add clickable legend state behavior
            this.legend.on('click', function(e) {
                $this.toggle(e);
            })
            .on('mouseover', function() {
                $this.legend.toggleClass('ui-state-hover');
            })
            .on('mouseout', function() {
                $this.legend.toggleClass('ui-state-hover');
            })
            .on('mousedown', function() {
                $this.legend.toggleClass('ui-state-active');
            })
            .on('mouseup', function() {
                $this.legend.toggleClass('ui-state-active');
            })
            .on('focus', function() {
                $this.legend.toggleClass('ui-state-focus');
            })
            .on('blur', function() {
                $this.legend.toggleClass('ui-state-focus');
            })
            .on('keydown', function(e) {
                var key = e.which,
                keyCode = $.ui.keyCode;

                if((key === keyCode.ENTER)) {
                    $this.toggle(e);
                    e.preventDefault();
                }
            });
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);

        this.loaded = false;
    },

    /**
     * Toggles the content of this fieldset (collapsed or expanded).
     * @param {JQuery.TriggeredEvent} [e] Optional event that triggered the toggling.
     */
    toggle: function(e) {
        var $this = this,
            collapsed = this.cfg.collapsed;

        if (!this.loaded && this.cfg.dynamic && collapsed) {
            this.loadContents();
        }
        else {
            this.updateToggleState(collapsed);
        }

        this.content.slideToggle(this.cfg.toggleSpeed, 'easeInOutCirc', function() {
            $this.callBehavior('toggle');
        });

        PrimeFaces.invokeDeferredRenders(this.id);
    },

    /**
     * Updates the visual toggler state and saves its state
     * @private
     * @param {boolean} collapsed If this fieldset is now collapsed or expanded.
     */
    updateToggleState: function(collapsed) {
        if(collapsed) {
            this.toggler.removeClass('ui-icon-plusthick').addClass('ui-icon-minusthick');
        }
        else {
            this.toggler.removeClass('ui-icon-minusthick').addClass('ui-icon-plusthick');
        }

        this.cfg.collapsed = !collapsed;

        this.stateHolder.val(!collapsed);
    },

    /**
     * Loads the contents of this fieldset panel dynamically via AJAX, if dynamic loading is enabled.
     * @private
     */
    loadContents: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                            $this.loaded = true;
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.updateToggleState($this.cfg.collapsed);
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    }

});
/**
 * __PrimeFaces InputText Widget__
 * 
 * InputText is an extension to standard inputText with skinning capabilities.
 * 
 * @prop {JQuery} counter The DOM element for the counter that informs the user about the number of characters they can
 * still enter before they reach the limit.
 * 
 * @interface {PrimeFaces.widget.InputTextCfg} cfg The configuration for the {@link  InputText| InputText 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 {string} cfg.counter ID of the label component to display remaining and entered characters.
 * @prop {string} cfg.counterTemplate Template text to display in counter, default value is `{0}`.
 */
PrimeFaces.widget.InputText = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        PrimeFaces.skinInput(this.jq);

        //Counter
        if(this.cfg.counter) {
            this.counter = this.cfg.counter ? $(PrimeFaces.escapeClientId(this.cfg.counter)) : null;
            this.cfg.counterTemplate = this.cfg.counterTemplate||'{0}';
            this.updateCounter();

            if(this.counter) {
                var $this = this;
                this.jq.on('input.inputtext-counter', function(e) {
                    $this.updateCounter();
                });
            }
        }
    },

    /**
     * Disabled this input field so that the user cannot enter text anymore.
     */
    disable: function() {
        this.jq.prop('disabled', true).addClass('ui-state-disabled');
    },

    /**
     * Enables this input field so that the user can enter text.
     */
    enable: function() {
        this.jq.prop('disabled', false).removeClass('ui-state-disabled');
    },

    /**
     * Updates the counter value that keeps count of how many more characters the user can enter before they reach the
     * limit.
     * @private
     */
    updateCounter: function() {
        var value = this.normalizeNewlines(this.jq.val()),
        length = value.length;

        if(this.counter && this.cfg.maxlength) {
            var remaining = this.cfg.maxlength - length;
            if(remaining < 0) {
                remaining = 0;
            }

            var counterText = this.cfg.counterTemplate
                    .replace('{0}', remaining)
                    .replace('{1}', length)
                    .replace('{2}', this.cfg.maxlength);

            this.counter.text(counterText);
        }
    },

    /**
     * Replaces all line breaks with a Window-style line break (carriage return + line feed).
     * @private
     * @param {string} text Text to normalize.
     * @return {string} The given text, with all line breaks replaced with carriage return + line feed. 
     */
    normalizeNewlines: function(text) {
        return text.replace(/(\r\n|\r|\n)/g, '\r\n');
    }
});

/**
 * __PrimeFaces InputTextarea Widget__
 * 
 * InputTextarea is an extension to standard inputTextarea with autoComplete, autoResize, remaining characters counter
 * and theming features.
 * 
 * @prop {JQuery} counter The DOM element for the counter that informs the user about the number of characters they can
 * still enter before they reach the limit.
 * @prop {JQuery} panel The DOM element for the overlay panel with the autocomplete suggestions.
 * @prop {JQuery} items The DOM elements in the autocomplete panel that the user can select.
 * @prop {number} timeout The internal timeout ID of the most recent timeout that was started.
 * 
 * @interface {PrimeFaces.widget.InputTextareaCfg} cfg The configuration for the {@link  InputTextarea| InputTextarea 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.DeferredWidgetCfg} cfg
 * 
 * @prop {boolean} cfg.autoResize Enables auto growing when being typed.
 * @prop {boolean} cfg.autoComplete Enables autocompletion that suggests tokens to the user as they type. 
 * @prop {string} cfg.counter ID of the label component to display remaining and entered characters.
 * @prop {string} cfg.counterTemplate Template text to display in counter, default value is `{0}`.
 * @prop {number} cfg.maxlength Maximum number of characters that may be entered in this field.
 * @prop {number} cfg.minQueryLength Number of characters to be typed to run a query.
 * @prop {number} cfg.queryDelay Delay in milliseconds before sending each query.
 * @prop {number} cfg.scrollHeight Height of the viewport for autocomplete suggestions.
 */
PrimeFaces.widget.InputTextarea = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        if(this.cfg.autoResize)
            this.renderDeferred();
        else
            this._render();
    },
    
    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        //Visuals
        PrimeFaces.skinInput(this.jq);

        //autoComplete
        if(this.cfg.autoComplete) {
            this.setupAutoComplete();
        }

        //Counter
        if(this.cfg.counter) {
            this.counter = this.cfg.counter ? $(PrimeFaces.escapeClientId(this.cfg.counter)) : null;
            this.cfg.counterTemplate = this.cfg.counterTemplate||'{0}';
            this.updateCounter();

            if(this.counter) {
                var $this = this;
                this.jq.on('input.inputtextarea-counter', function(e) {
                    $this.updateCounter();
                });
            }
        }

        //maxLength
        if(this.cfg.maxlength) {
            this.applyMaxlength();
        }

        //autoResize
        if(this.cfg.autoResize) {
            this.setupAutoResize();
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        //remove autocomplete panel
        if(cfg.autoComplete) {
            $(PrimeFaces.escapeClientId(cfg.id + '_panel')).remove();
        }

        this._super(cfg);
    },

    /**
     * Initializes the auto resize functionality that resize this textarea depending on the entered text.
     * @private
     */
    setupAutoResize: function() {
        autosize(this.jq);
    },

    /**
     * Applies the value of the max length setting, counting line breaks correctly.
     * @private
     */
    applyMaxlength: function() {
        var _self = this;

        this.jq.on('keyup.inputtextarea-maxlength', function(e) {
            var value = _self.jq.val(),
            length = value.length;

            if(length > _self.cfg.maxlength) {
                _self.jq.val(value.substr(0, _self.cfg.maxlength));
            }
        });
    },

    /**
     * Updates the counter value that keeps count of how many more characters the user can enter before they reach the
     * limit.
     * @private
     */
    updateCounter: function() {
        var value = this.jq.val(),
        length = value.length;

        if(this.counter) {
            var remaining = this.cfg.maxlength - length;
            if(remaining < 0) {
                remaining = 0;
            }

            var counterText = this.cfg.counterTemplate
                    .replace('{0}', remaining)
                    .replace('{1}', length)
                    .replace('{2}', this.cfg.maxlength);

            this.counter.text(counterText);
        }
    },

    /**
     * Sets up the server-side auto complete functionality that suggests tokens while the user types.
     * @private
     */
    setupAutoComplete: function() {
        var panelMarkup = '<div id="' + this.id + '_panel" class="ui-autocomplete-panel ui-widget-content ui-corner-all ui-helper-hidden ui-shadow"></div>',
        _self = this;

        this.panel = $(panelMarkup).appendTo(document.body);

        this.jq.on("keyup", function(e) {
            var keyCode = $.ui.keyCode;

            switch(e.which) {

                case keyCode.UP:
                case keyCode.LEFT:
                case keyCode.DOWN:
                case keyCode.RIGHT:
                case keyCode.ENTER:
                case keyCode.TAB:
                case keyCode.SPACE:
                case 17: //keyCode.CONTROL:
                case 18: //keyCode.ALT:
                case keyCode.ESCAPE:
                case 224:   //mac command
                    //do not search
                break;

                default:
                    var query = _self.extractQuery();
                    if(query && query.length >= _self.cfg.minQueryLength) {

                         //Cancel the search request if user types within the timeout
                        if(_self.timeout) {
                            _self.clearTimeout(_self.timeout);
                        }

                        _self.timeout = setTimeout(function() {
                            _self.search(query);
                        }, _self.cfg.queryDelay);

                    }
                break;
            }

        }).on("keydown", function(e) {
            var overlayVisible = _self.panel.is(':visible'),
            keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.UP:
                case keyCode.LEFT:
                    if(overlayVisible) {
                        var highlightedItem = _self.items.filter('.ui-state-highlight'),
                        prev = highlightedItem.length == 0 ? _self.items.eq(0) : highlightedItem.prev();

                        if(prev.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            prev.addClass('ui-state-highlight');

                            if(_self.cfg.scrollHeight) {
                                PrimeFaces.scrollInView(_self.panel, prev);
                            }
                        }

                        e.preventDefault();
                    }
                    else {
                        _self.clearTimeout();
                    }
                break;

                case keyCode.DOWN:
                case keyCode.RIGHT:
                    if(overlayVisible) {
                        var highlightedItem = _self.items.filter('.ui-state-highlight'),
                        next = highlightedItem.length == 0 ? _self.items.eq(0) : highlightedItem.next();

                        if(next.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            next.addClass('ui-state-highlight');

                            if(_self.cfg.scrollHeight) {
                                PrimeFaces.scrollInView(_self.panel, next);
                            }
                        }

                        e.preventDefault();
                    }
                    else {
                        _self.clearTimeout();
                    }
                break;

                case keyCode.ENTER:
                    if(overlayVisible) {
                        _self.items.filter('.ui-state-highlight').trigger('click');

                        e.preventDefault();
                    }
                    else {
                        _self.clearTimeout();
                    }
                break;

                case keyCode.SPACE:
                case 17: //keyCode.CONTROL:
                case 18: //keyCode.ALT:
                case keyCode.BACKSPACE:
                case keyCode.ESCAPE:
                case 224:   //mac command
                    _self.clearTimeout();

                    if(overlayVisible) {
                        _self.hide();
                    }
                break;

                case keyCode.TAB:
                    _self.clearTimeout();

                    if(overlayVisible) {
                        _self.items.filter('.ui-state-highlight').trigger('click');
                        _self.hide();
                    }
                break;
            }
        });

        //hide panel when outside is clicked
        $(document.body).on('mousedown.ui-inputtextarea', function (e) {
            if(_self.panel.is(":hidden")) {
                return;
            }
            var offset = _self.panel.offset();
            if(e.target === _self.jq.get(0)) {
                return;
            }

            if (e.pageX < offset.left ||
                e.pageX > offset.left + _self.panel.width() ||
                e.pageY < offset.top ||
                e.pageY > offset.top + _self.panel.height()) {
                _self.hide();
            }
        });

        //Hide overlay on resize
        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', _self.panel, function() {
            _self.hide();
        });

        //dialog support
        this.setupDialogSupport();
    },

    /**
     * Sets up all event listeners for the various events required by this widget.
     * @private
     */
    bindDynamicEvents: function() {
        var _self = this;

        //visuals and click handler for items
        this.items.on('mouseover', function() {
            var item = $(this);

            if(!item.hasClass('ui-state-highlight')) {
                _self.items.filter('.ui-state-highlight').removeClass('ui-state-highlight');
                item.addClass('ui-state-highlight');
            }
        })
        .on('click', function(event) {
            var item = $(this),
            itemValue = item.attr('data-item-value'),
            selectionStart = _self.jq.getSelection().start,
            queryLength = _self.query.length;

            _self.jq.trigger('focus');

            _self.jq.setSelection(selectionStart-queryLength, selectionStart);
            _self.jq.replaceSelectedText(itemValue);

            _self.invokeItemSelectBehavior(event, itemValue);

            _self.hide();
        });
    },

    /**
     * Callback that is invoked when the user has selected one of the suggested tokens.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that triggered the item selection (usually a click or enter press).
     * @param {string} itemValue Value of the suggestion that was selected.
     */
    invokeItemSelectBehavior: function(event, itemValue) {
        if(this.hasBehavior('itemSelect')) {
            var ext = {
                params : [
                    {name: this.id + '_itemSelect', value: itemValue}
                ]
            };

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

    /**
     * Clears the timeout that was set up by the autocomplete feature.
     * @private
     */
    clearTimeout: function() {
        if(this.timeout) {
            clearTimeout(this.timeout);
        }

        this.timeout = null;
    },

    /**
     * Finds the keyword to be used for the autocomplete search.
     * @private
     * @return {string} The keyword or search term the autocomplete method receives as input.
     */
    extractQuery: function() {
        var end = this.jq.getSelection().end,
        result = /\S+$/.exec(this.jq.get(0).value.slice(0, end)),
        lastWord = result ? result[0] : null;

        return lastWord;
    },

    /**
     * Performs an autocomplete search for the given search term. Opens the windows with the suggestions.
     * @param {string} query Search term to search for.
     */
    search: function(query) {
        this.query = query;

        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            params: [
                {name: this.id + '_query', value: query}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.panel.html(content);
                            this.items = $this.panel.find('.ui-autocomplete-item');

                            this.bindDynamicEvents();

                            if(this.items.length > 0) {
                                //highlight first item
                                this.items.eq(0).addClass('ui-state-highlight');

                                //adjust height
                                if(this.cfg.scrollHeight && this.panel.height() > this.cfg.scrollHeight) {
                                    this.panel.height(this.cfg.scrollHeight);
                                }

                                if(this.panel.is(':hidden')) {
                                    this.show();
                                }  else {
                                    this.alignPanel(); //with new items
                                }

                            }
                            else {
                                this.panel.hide();
                            }
                        }
                    });

                return true;
            }
        };

        if (this.hasBehavior('query')) {
            this.callBehavior('query', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Aligns the search window panel of the autocomplete feature.
     */
    alignPanel: function() {
        var pos = this.jq.getCaretPosition(),
        posLeft = (pos.left > 0 ? '+' : '-') + pos.left,
        posTop = (pos.top > 0 ? '+' : '-') + pos.top;

        this.panel.css({left:'', top:''}).position({
            my: 'left top'
            ,at: 'left' + posLeft + 'px' +  ' top' + posTop + 'px'
            ,of: this.jq
        });
    },

    /**
     * Brings up the search window panel of the autocomplete feature.
     * @private
     */
    show: function() {
        this.panel.css({
            'z-index': PrimeFaces.nextZindex(),
            'width': this.jq.innerWidth() + 'px',
            'visibility': 'hidden'
        }).show();

        this.alignPanel();

        this.panel.css('visibility', '');
    },

    /**
     * Hides the search window panel of the autocomplete feature.
     * @private
     */
    hide: function() {
        this.panel.hide();
    },

    /**
     * Adjust the search window panel of the autocomplete in case this widget is inside a dialog overlay.
     * @private
     */
    setupDialogSupport: function() {
        var dialog = this.jq.parents('.ui-dialog:first');

        if(dialog.length == 1 && dialog.css('position') === 'fixed') {
            this.panel.css('position', 'fixed');
        }
    }

});

/**
 * __PrimeFaces SelectOneMenu Widget__
 * 
 * SelectOneMenu is an extended version of the standard SelectOneMenu.
 * 
 * @typedef {"slow" | "normal" | "fast"} PrimeFaces.widget.SelectOneMenu.EffectSpeed Duration of toggle animation of the
 * overlay panel.
 * 
 * @typedef {"startsWith" |  "contains" |  "endsWith" | "custom"} PrimeFaces.widget.SelectOneMenu.FilterMatchMode
 * Available modes for filtering the options of a select list box. When `custom` is set, a `filterFunction` must be
 * specified.
 * 
 * @typedef PrimeFaces.widget.SelectOneMenu.FilterFunction A function for filtering the options of a select list box.
 * @param {string} PrimeFaces.widget.SelectOneMenu.FilterFunction.itemLabel The label of the currently selected text.
 * @param {string} PrimeFaces.widget.SelectOneMenu.FilterFunction.filterValue The value to search for.
 * @return {boolean} PrimeFaces.widget.SelectOneMenu.FilterFunction `true` if the item label matches the filter value,
 * or `false` otherwise.
 * 
 * @prop {boolean} changed Whether the value of this widget was changed from its original value.
 * @prop {JQuery} customInput The DOM element for the input field that lets the user enter a custom value which does not
 * have to match one of the available options.
 * @prop {string} customInputVal The custom value that was entered by the user which does not have to match one the
 * available options.
 * @prop {boolean} disabled Whether this widget is currently disabled.
 * @prop {JQuery} filterInput The DOM element for the input field that lets the user enter a search term to filter the
 * list of available options.
 * @prop {PrimeFaces.widget.SelectOneMenu.FilterFunction} filterMatcher The filter that was selected and is
 * currently used.
 * @prop {Record<PrimeFaces.widget.SelectOneMenu.FilterMatchMode, PrimeFaces.widget.SelectOneMenu.FilterFunction>} filterMatchers
 * Map between the available filter types and the filter implementation.
 * @prop {JQuery} input The DOM element for the hidden input with the current value.
 * @prop {boolean} isDynamicLoaded Whether the contents of the overlay panel were loaded.
 * @prop {JQuery} items The DOM elements for the the available selectable options.
 * @prop {JQuery} itemContainer The DOM element for the container with the available selectable options.
 * @prop {JQuery} itemContainerWrapper The DOM element for the wrapper with the container with the available selectable
 * options.
 * @prop {JQuery} focusInput The hidden input that can be focused via the tab key etc.
 * @prop {JQuery} label The DOM element for the label indicating the currently selected option.
 * @prop {JQuery} menuIcon The DOM element for the icon for bringing up the overlay panel.
 * @prop {JQuery} options The DOM elements for the available selectable options.
 * @prop {number} optGroupsSize The number of option groups.
 * @prop {JQuery} panel The DOM element for the overlay panel with the available selectable options.
 * @prop {JQuery} panelId ID of the DOM element for the overlay panel with the available selectable options.
 * @prop {number} panelWidthAdjusted The adjusted width of the overlay panel.
 * @prop {JQuery} preShowValue The DOM element for the selected option that is shown before the overlay panel is brought
 * up.
 * @prop {number} searchTimer ID of the timeout for the delay of the filter input in the overlay panel.
 * @prop {JQuery} triggers The DOM elements for the buttons that can trigger (hide or show) the overlay panel with the
 * available selectable options.
 * @prop {string} value The current value of this select one menu.
 * 
 * @interface {PrimeFaces.widget.SelectOneMenuCfg} cfg The configuration for the {@link  SelectOneMenu| SelectOneMenu 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.DeferredWidgetCfg} cfg
 * 
 * @prop {boolean} cfg.alwaysDisplayLabel `true` if the label of the selected item should always be set on the visible
 * input, `false` otherwise.
 * @prop {string} cfg.appendTo Appends the overlay to the element defined by search expression. Defaults to the document
 * body.
 * @prop {boolean} cfg.autoWidth Calculates a fixed width based on the width of the maximum option label. Set to false
 * for custom width.
 * @prop {boolean} cfg.caseSensitive Defines if filtering would be case sensitive.
 * @prop {boolean} cfg.dynamic Defines if dynamic loading is enabled for the element's panel. If the value is `true`,
 * the overlay is not rendered on page load to improve performance.
 * @prop {boolean} cfg.editable When true, the input field becomes editable.
 * @prop {boolean} cfg.filter `true` if the options can be filtered, or `false` otherwise.
 * @prop {PrimeFaces.widget.SelectOneMenu.FilterFunction} cfg.filterFunction A custom filter function that is used
 * when `filterMatchMode` is set to `custom`.
 * @prop {PrimeFaces.widget.SelectOneMenu.FilterMatchMode} cfg.filterMatchMode Mode of the filter. When set to
 * `custom` a `filterFunction` must be specified.
 * @prop {number} cfg.initialHeight Initial height of the overlay panel in pixels.
 * @prop {string} cfg.label Text of the label for the input.
 * @prop {string} cfg.labelTemplate Displays label of the element in a custom template. Valid placeholder is `{0}`,
 * which is replaced with the value of the currently selected item.
 * @prop {boolean} cfg.syncTooltip Updates the title of the component with the description of the selected item.
 * @prop {boolean} cfg.renderPanelContentOnClient Renders panel content on client.
 */
PrimeFaces.widget.SelectOneMenu = PrimeFaces.widget.DeferredWidget.extend({

	/**
	 * @override
	 * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
	 */
    init: function(cfg) {
        this._super(cfg);

        this.panelId = this.jqId + '_panel';
        this.input = $(this.jqId + '_input');
        this.focusInput = $(this.jqId + '_focus');
        this.label = this.jq.find('.ui-selectonemenu-label');
        this.menuIcon = this.jq.children('.ui-selectonemenu-trigger');

        this.panel = $(this.panelId);
        this.disabled = this.jq.hasClass('ui-state-disabled');
        this.itemsWrapper = this.panel.children('.ui-selectonemenu-items-wrapper');
        this.options = this.input.find('option');
        this.cfg.effect = this.cfg.effect||'fade';

        this.cfg.effectSpeed = this.cfg.effectSpeed||'normal';
        this.cfg.autoWidth = this.cfg.autoWidth === false ? false : true;
        this.cfg.dynamic = this.cfg.dynamic === true ? true : false;
        this.cfg.appendTo = PrimeFaces.utils.resolveAppendTo(this, this.panel);
        this.cfg.renderPanelContentOnClient = this.cfg.renderPanelContentOnClient === true;
        this.isDynamicLoaded = false;

        if(this.cfg.dynamic || (this.itemsWrapper.children().length === 0)) {
            var selectedOption = this.options.filter(':selected'),
            labelVal = this.cfg.editable ? this.label.val() : selectedOption.text();

            this.setLabel(labelVal);
        }
        else {
            this.initContents();
            this.bindItemEvents();
        }

        //triggers
        this.triggers = this.cfg.editable ? this.jq.find('.ui-selectonemenu-trigger') : this.jq.find('.ui-selectonemenu-trigger, .ui-selectonemenu-label');

        //mark trigger and descandants of trigger as a trigger for a primefaces overlay
        this.triggers.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

        if(!this.disabled) {
            this.bindEvents();

            PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');
            this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
        }

        // see #7602
        if (PrimeFaces.env.isTouchable(this.cfg)) {
            this.focusInput.attr('readonly', true);
        }

        this.renderDeferred();
    },

    /**
     * Finds and initializes the DOM elements that make up this widget.
     * @private
     */
    initContents: function() {
        this.itemsContainer = this.itemsWrapper.children('.ui-selectonemenu-items');
        this.items = this.itemsContainer.find('.ui-selectonemenu-item');
        this.optGroupsSize = this.itemsContainer.children('li.ui-selectonemenu-item-group').length;

        var $this = this,
        selectedOption = this.options.filter(':selected'),
        highlightedItem = this.items.eq(this.options.index(selectedOption));

        //disable options
        this.options.filter(':disabled').each(function() {
            $this.items.eq($(this).index()).addClass('ui-state-disabled');
        });

        //activate selected
        if(this.cfg.editable) {
            var customInputVal = this.label.val();

            //predefined input
            if(customInputVal === selectedOption.text()) {
                this.highlightItem(highlightedItem);
            }
            //custom input
            else {
                this.items.eq(0).addClass('ui-state-highlight');
                this.customInput = true;
                this.customInputVal = customInputVal;
            }
        }
        else {
            this.highlightItem(highlightedItem);
        }

        if(this.cfg.syncTooltip) {
            this.syncTitle(selectedOption);
        }

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);

        //for Screen Readers
        for(var i = 0; i < this.items.length; i++) {
            this.items.eq(i).attr('id', this.id + '_' + i);
        }

        var highlightedItemId = highlightedItem.attr('id');
        this.jq.attr('aria-owns', this.itemsContainer.attr('id'));
        this.focusInput.attr('aria-autocomplete', 'list')
            .attr('aria-activedescendant', highlightedItemId)
            .attr('aria-describedby', highlightedItemId)
            .attr('aria-disabled', this.disabled);
        this.itemsContainer.attr('aria-activedescendant', highlightedItemId);
    },

    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        var contentStyle = this.jq.attr('style'),
        hasWidth = contentStyle && contentStyle.indexOf('width') != -1;

        if(this.cfg.autoWidth && !hasWidth) {
            this.jq.css('min-width', this.input.outerWidth() + 'px');
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.panelWidthAdjusted = false;
        this.items = null;

        this._super(cfg);
    },

    /**
     * Adjust the width of the overlay panel.
     * @private 
     */
    alignPanelWidth: function() {
        //align panel and container
        if(!this.panelWidthAdjusted) {
            var jqWidth = this.jq.outerWidth();
            if(this.panel.outerWidth() < jqWidth) {
                this.panel.width(jqWidth);
            }
            else {
                this.panel.width(this.panel.width());
            }

            this.panelWidthAdjusted = true;
        }
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        // Screen Reader(JAWS) hack on Chrome
        if(PrimeFaces.env.browser.webkit) {
            this.input.on('focus', function(){
                setTimeout(function(){
                    $this.focusInput.trigger('focus.ui-selectonemenu');
                },2);
            });
        }

        //Triggers
        this.triggers.on("mouseenter", function() {
            if(!$this.jq.hasClass('ui-state-focus')) {
                $this.jq.addClass('ui-state-hover');
                $this.menuIcon.addClass('ui-state-hover');
            }
        })
        .on("mouseleave", function() {
            $this.jq.removeClass('ui-state-hover');
            $this.menuIcon.removeClass('ui-state-hover');
        })
        .on("click", function(e) {
            if($this.panel.is(":hidden")) {
                $this.show();
            }
            else {
                $this.hide();

                $this.revert();
                $this.changeAriaValue($this.getActiveItem());
            }

            $this.jq.removeClass('ui-state-hover');
            $this.menuIcon.removeClass('ui-state-hover');
            $this.focusInput.trigger('focus.ui-selectonemenu');
            e.preventDefault();
        });

        this.focusInput.on('focus.ui-selectonemenu', function() {
            $this.jq.addClass('ui-state-focus');
            $this.menuIcon.addClass('ui-state-focus');
        })
        .on('blur.ui-selectonemenu', function(){
            $this.jq.removeClass('ui-state-focus');
            $this.menuIcon.removeClass('ui-state-focus');

            $this.callBehavior('blur');
        });

        //onchange handler for editable input
        if(this.cfg.editable) {
            this.label.on('change', function(e) {
                $this.triggerChange(true);
                $this.callHandleMethod($this.handleLabelChange, e);
            });
        }

        //key bindings
        this.bindKeyEvents();

        //filter
        if(this.cfg.filter) {
            this.cfg.initialHeight = this.itemsWrapper.height();
            this.setupFilterMatcher();
            this.filterInput = this.panel.find('> div.ui-selectonemenu-filter-container > input.ui-selectonemenu-filter');
            PrimeFaces.skinInput(this.filterInput);

            this.bindFilterEvents();
        }
    },

    /**
     * Sets up the event listeners for the selectable items.
     * @private
     */
    bindItemEvents: function() {
        var $this = this;
        if(!this.items) {
            return;
        }

        //Items
        this.items.filter(':not(.ui-state-disabled)').on('mouseover.selectonemenu', function() {
            var el = $(this);

            if(!el.hasClass('ui-state-highlight'))
                $(this).addClass('ui-state-hover');
        })
        .on('mouseout.selectonemenu', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click.selectonemenu', function() {
            $this.revert();
            $this.selectItem($(this));
            $this.changeAriaValue($(this));
        });
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.panel,
            function() { return  $this.label.add($this.menuIcon); },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    $this.hide();
                    setTimeout(function() {
                        $this.revert();
                        $this.changeAriaValue($this.getActiveItem());
                    }, 2);
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.panel, function() {
            $this.hide();
        });

        // GitHub #1173/#4609 keep panel with select while scrolling
        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Removes some event listeners when this widget was disabled.
     * @private
     */
    unbindEvents: function() {
        if (this.items) {
            this.items.off();
        }
        this.triggers.off();
        this.input.off();
        this.focusInput.off();
        this.label.off();
    },

    /**
     * Unselect the selected item, if any, and select the `please select` option.
     */
    revert: function() {
        if(this.cfg.editable && this.customInput) {
            this.setLabel(this.customInputVal);
            this.items.filter('.ui-state-active').removeClass('ui-state-active');
            this.items.eq(0).addClass('ui-state-active');
        }
        else {
            this.highlightItem(this.items.eq(this.options.index(this.preShowValue)));
        }
    },

    /**
     * Highlight the given selectable option.
     * @private
     * @param {JQuery} item Option to highlight.
     */
    highlightItem: function(item) {
        this.items.attr('aria-selected', false);
        this.items.filter('.ui-state-highlight').removeClass('ui-state-highlight');

        if(item.length > 0) {
            item.addClass('ui-state-highlight');
            item.attr('aria-selected', true);
            this.setLabel(item.data('label'));
        }
    },

    /**
     * Triggers the event listeners when the value of this widget changed.
     * @private
     * @param {boolean} edited Whether the value was edited by the user. If it was, checks which option is now selected.
     */
    triggerChange: function(edited) {
        this.changed = false;

        this.input.trigger('change');

        if(!edited) {
            this.value = this.options.filter(':selected').val();
        }
    },

    /**
     * Callback for when the user selects an item with the mouse.
     * @private
     * @param {JQuery} item The option to select.
     * @param {boolean} silent `true` to suppress triggering event listeners, or `false` otherwise.
     */
    selectItem: function(item, silent) {
        var selectedOption = this.options.eq(this.resolveItemIndex(item)),
        currentOption = this.options.filter(':selected'),
        sameOption = selectedOption.val() == currentOption.val(),
        shouldChange = null;

        if(this.cfg.editable) {
            shouldChange = (!sameOption)||(selectedOption.text() != this.label.val());
        }
        else {
            shouldChange = !sameOption;
        }

        if(shouldChange) {
            this.highlightItem(item);
            this.input.val(selectedOption.val())

            this.triggerChange();

            if(this.cfg.editable) {
                this.customInput = false;
            }

            if(this.cfg.syncTooltip) {
                this.syncTitle(selectedOption);
            }
        }

        if(!silent) {
            this.callBehavior('itemSelect');
            this.focusInput.trigger('focus');
        }

        if(this.panel.is(':visible')) {
            this.hide();
        }
    },

    /**
     * Adjust the value of the title attribute to match selected option.
     * @private
     * @param {JQuery} option The option that was selected.
     */
    syncTitle: function(option) {
        var optionTitle = this.items.eq(option.index()).attr('title');
        if(optionTitle)
            this.jq.attr('title', this.items.eq(option.index()).attr('title'));
        else
            this.jq.removeAttr('title');
    },

    /**
     * Finds the index of the given selectable option.
     * @param {JQuery} item One of the available selectable options.
     * @return {number} The index of the given item. 
     */
    resolveItemIndex: function(item) {
        if(this.optGroupsSize === 0)
            return item.index();
        else
            return item.index() - item.prevAll('li.ui-selectonemenu-item-group').length;
    },

    /**
     * Sets up the event listeners for all keyboard related events other than the overlay panel, such as pressing space
     * to bring up the overlay panel.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        this.focusInput.on('keydown.ui-selectonemenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                case keyCode.LEFT:
                    $this.callHandleMethod($this.highlightPrev, e);
                break;

                case keyCode.DOWN:
                case keyCode.RIGHT:
                    $this.callHandleMethod($this.highlightNext, e);
                break;

                case keyCode.ENTER:
                    $this.handleEnterKey(e);
                break;

                case keyCode.TAB:
                    $this.handleTabKey();
                break;

                case keyCode.ESCAPE:
                    $this.handleEscapeKey(e);
                break;

                case keyCode.SPACE:
                    $this.handleSpaceKey(e);
                break;
            }
        })
        .on('keyup.ui-selectonemenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                case keyCode.LEFT:
                case keyCode.DOWN:
                case keyCode.RIGHT:
                case keyCode.ENTER:
                case keyCode.TAB:
                case keyCode.ESCAPE:
                case keyCode.SPACE:
                case keyCode.HOME:
                case keyCode.PAGE_DOWN:
                case keyCode.PAGE_UP:
                case keyCode.END:
                case keyCode.DELETE:
                case 16: //shift
                case 17: //keyCode.CONTROL:
                case 18: //keyCode.ALT:
                case 19: //Pause/Break:
                case 20: //capslock:
                case 44: //Print Screen:
                case 45: //Insert:
                case 91: //left window or cmd:
                case 92: //right window:
                case 93: //right cmd:
                case 144: //num lock:
                case 145: //scroll lock:
                break;

                default:
                    //function keys (F1,F2 etc.)
                    if(key >= 112 && key <= 123) {
                        break;
                    }

                    var matchedOptions = null,
                    metaKey = e.metaKey||e.ctrlKey||e.altKey;

                    if(!metaKey) {
                        clearTimeout($this.searchTimer);

                        // #4682: check for word match
                        var text = $(this).val();
                        matchedOptions = $this.matchOptions(text);
                        if(matchedOptions.length) {
                            var matchIndex = matchedOptions[0].index;
                            var highlightItem = $this.items.eq(matchIndex);
                            if($this.panel.is(':hidden')) {
                                $this.selectItem(highlightItem);
                            }
                            else {
                                $this.highlightItem(highlightItem);
                                PrimeFaces.scrollInView($this.itemsWrapper, highlightItem);
                            }
                        } else {
                            // #4682: check for first letter match
                            text = String.fromCharCode(key).toLowerCase();
                            // find all options with the same first letter
                            matchedOptions = $this.matchOptions(text);
                            if(matchedOptions.length) {
                                var selectedIndex = -1;

                                // is current selection one of our matches?
                                matchedOptions.each(function() {
                                   var option = $(this);
                                   var currentIndex = option[0].index;
                                   var currentItem = $this.items.eq(currentIndex);
                                   if (currentItem.hasClass('ui-state-highlight')) {
                                       selectedIndex = currentIndex;
                                       return false;
                                   }
                                });

                                matchedOptions.each(function() {
                                    var option = $(this);
                                    var currentIndex = option[0].index;
                                    var currentItem = $this.items.eq(currentIndex);

                                    // select next item after the current selection
                                    if (currentIndex > selectedIndex) {
                                         if($this.panel.is(':hidden')) {
                                             $this.selectItem(currentItem);
                                         }
                                         else {
                                             $this.highlightItem(currentItem);
                                             PrimeFaces.scrollInView($this.itemsWrapper, currentItem);
                                         }
                                         return false;
                                     }
                                });
                            }
                        }

                        $this.searchTimer = setTimeout(function(){
                            $this.focusInput.val('');
                        }, 1000);
                    }
                break;
            }
        });
    },

    /**
     * Finds all options that match the given search string.
     * @private
     * @param {string} text The search string against which to match the options.
     * @return {JQuery} All selectable options that match (contain) the given search string. 
     */
    matchOptions: function(text) {
        if(!text) {
            return false;
        }
        return this.options.filter(function() {
            var option = $(this);
            if(option.is(':disabled')) {
                return false;
            }
            if(option.text().toLowerCase().indexOf(text.toLowerCase()) !== 0) {
                return false;
            }
            return true;
        });
    },

    /**
     * Sets up the event listeners for the filter input in the overlay panel.
     * @private
     */
    bindFilterEvents: function() {
        var $this = this;

        this.filterInput.on('keyup.ui-selectonemenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                case keyCode.LEFT:
                case keyCode.DOWN:
                case keyCode.RIGHT:
                case keyCode.ENTER:
                case keyCode.TAB:
                case keyCode.ESCAPE:
                case keyCode.SPACE:
                case keyCode.HOME:
                case keyCode.PAGE_DOWN:
                case keyCode.PAGE_UP:
                case keyCode.END:
                case 16: //shift
                case 17: //keyCode.CONTROL:
                case 18: //keyCode.ALT:
                case 91: //left window or cmd:
                case 92: //right window:
                case 93: //right cmd:
                case 20: //capslock:
                break;

                default:
                    //function keys (F1,F2 etc.)
                    if(key >= 112 && key <= 123) {
                        break;
                    }

                    var metaKey = e.metaKey||e.ctrlKey;

                    if(!metaKey) {
                        $this.filter($(this).val());
                    }
                break;
            }
        })
        .on('keydown.ui-selectonemenu',function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                    $this.highlightPrev(e);
                break;

                case keyCode.DOWN:
                    $this.highlightNext(e);
                break;

                case keyCode.ENTER:
                    $this.handleEnterKey(e);
                break;

                case keyCode.TAB:
                    $this.handleTabKey();
                break;

                case keyCode.ESCAPE:
                    $this.handleEscapeKey(e);
                break;

                case keyCode.SPACE:
                    $this.handleSpaceKey(e);
                break;

                default:
                break;
            }
        }).on('paste.ui-selectonemenu', function() {
            setTimeout(function(){
                $this.filter($this.filterInput.val());
            },2);
		});
    },

    /**
     * Highlights the next option after the currently highlighted option in the overlay panel.
     * @private
     * @param {JQuery.TriggeredEvent} event The event of the keypress.
     */
    highlightNext: function(event) {
        var activeItem = this.getActiveItem(),
        next = this.panel.is(':hidden') ? activeItem.nextAll(':not(.ui-state-disabled,.ui-selectonemenu-item-group):first')
                                : activeItem.nextAll(':not(.ui-state-disabled,.ui-selectonemenu-item-group):visible:first');

        if(event.altKey) {
            this.show();
        }
        else {
            if(next.length === 1) {
                if(this.panel.is(':hidden')) {
                    this.selectItem(next);
                }
                else {
                    this.highlightItem(next);
                    PrimeFaces.scrollInView(this.itemsWrapper, next);
                }
                this.changeAriaValue(next);
            }
        }

        event.preventDefault();
    },

    /**
     * Highlights the previous option before the currently highlighted option in the overlay panel.
     * @private
     * @param {JQuery.TriggeredEvent} event The event of the keypress.
     */
    highlightPrev: function(event) {
        var activeItem = this.getActiveItem(),
        prev = this.panel.is(':hidden') ? activeItem.prevAll(':not(.ui-state-disabled,.ui-selectonemenu-item-group):first')
                                : activeItem.prevAll(':not(.ui-state-disabled,.ui-selectonemenu-item-group):visible:first');

        if(prev.length === 1) {
            if(this.panel.is(':hidden')) {
                this.selectItem(prev);
            }
            else {
                this.highlightItem(prev);
                PrimeFaces.scrollInView(this.itemsWrapper, prev);
            }
            this.changeAriaValue(prev);
        }

        event.preventDefault();
    },

    /**
     * Callback for when the enter key was pressed. Brings up the overlay panel or accepts the highlighted option.
     * @private
     * @param {JQuery.TriggeredEvent} event The event of the keypress.
     */
    handleEnterKey: function(event) {
        if(this.panel.is(':visible')) {
            this.selectItem(this.getActiveItem());
        }

        event.preventDefault();
        event.stopPropagation();
    },

    /**
     * Callback for when the space key was pressed. Brings up or hides the overlay panel.
     * @private
     * @param {JQuery.TriggeredEvent} event The event of the keypress.
     */
    handleSpaceKey: function(event) {
        var target = $(event.target);

        if(target.is('input') && target.hasClass('ui-selectonemenu-filter')) {
            return;
        }

        if(this.panel.is(":hidden")) {
            this.show();
        }
        else {
            this.hide();

            this.revert();
            this.changeAriaValue(this.getActiveItem());
        }

        event.preventDefault();
    },

    /**
     * Callback for when the escape key was pressed. Hides the overlay panel.
     * @private
     * @param {JQuery.TriggeredEvent} event The event of the keypress.
     */
    handleEscapeKey: function(event) {
        if(this.panel.is(':visible')) {
            this.revert();
            this.hide();
        }

        event.preventDefault();
    },

    /**
     * Callback for when the tab key was pressed. Selects the next option.
     * @private
     */
    handleTabKey: function() {
        if(this.panel.is(':visible')) {
            this.selectItem(this.getActiveItem());
        }
    },

    /**
     * Callback that adjusts the label, invoked when the selected option has changed.
     * @private
     * @param {JQuery.TriggeredEvent} event The event that triggered the change.
     */
    handleLabelChange: function(event) {
        this.customInput = true;
        this.customInputVal = $(event.target).val();
        this.items.filter('.ui-state-active').removeClass('ui-state-active');
        this.items.eq(0).addClass('ui-state-active');
    },

    /**
     * Brings up the overlay panel with the available selectable options.
     */
    show: function() {
        this.callHandleMethod(this._show, null);
    },

    /**
     * Brings up the overlay panel with the available selectable options. Compared this `show`, this does not ensure
     * the the overlay panel is loaded already (when dynamic loading is enabled).
     * @private
     */
    _show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.panel.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.bindPanelEvents();

                    //value before panel is shown
                    $this.preShowValue = $this.options.filter(':selected');
                    $this.jq.attr('aria-expanded', true);

                    PrimeFaces.scrollInView($this.itemsWrapper, $this.getActiveItem());

                    if ($this.cfg.filter) {
                        $this.focusFilter();
                    }
                }
            });
        }
    },

    /**
     * Hides the overlay panel with the available selectable options.
     */
    hide: function() {
        if (this.panel.is(':visible') && this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.panel.css('z-index', '');
                    $this.jq.attr('aria-expanded', false);
                }
            });
        }
    },

    /**
     * Puts focus on this widget.
     */
    focus: function() {
        this.focusInput.trigger('focus');
    },

    /**
     * Puts focus on the filter input in the overlay panel.
     * @param {number | undefined} timeout Amount of time in milliseconds to wait before attempting to focus the input. 
     */
    focusFilter: function(timeout) {
        if(timeout) {
            var $this = this;
            setTimeout(function() {
                $this.focusFilter();
            }, timeout);
        }
        else {
            this.filterInput.trigger('focus');
        }
    },

    /**
     * Removes focus from this widget.
     */
    blur: function() {
        this.focusInput.trigger("blur");

        this.callBehavior('blur');
    },

    /**
     * Disables this widget so that the user cannot select any option.
     */
    disable: function() {
    	if (!this.disabled) {
	        this.disabled = true;
	        this.jq.addClass('ui-state-disabled');
	        this.input.attr('disabled', 'disabled');
	        if(this.cfg.editable) {
	            this.label.attr('disabled', 'disabled');
	        }
	        this.unbindEvents();
    	}
    },

    /**
     * Enables this widget so that the user can select an option.
     */
    enable: function() {
    	if (this.disabled) {
	        this.disabled = false;
	        this.jq.removeClass('ui-state-disabled');
	        this.input.removeAttr('disabled');
	        if(this.cfg.editable) {
	            this.label.removeAttr('disabled');
	        }

            this.bindEvents();
            this.bindItemEvents();
    	}
    },

    /**
     * Align the overlay panel with the available selectable options so that is is positioned next to the the button.
     */
    alignPanel: function() {
        this.alignPanelWidth();

        if(this.panel.parent().is(this.jq)) {
            this.panel.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px',
                'transform-origin': 'center top'
            });
        }
        else {
            this.panel.css({left:'0px', top:'0px', 'transform-origin': 'center top'}).position({
                my: 'left top'
                ,at: 'left bottom'
                ,of: this.jq
                ,collision: 'flipfit'
                ,using: function(pos, directions) {
                    $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                }
            });
        }
    },

    /**
     * Sets the label text that indicates the currently selected item to the item with the given value.
     * @private
     * @param {string} value Value of the item that was selected.
     */
    setLabel: function(value) {
        var displayedLabel = this.getLabelToDisplay(value);

        if (this.cfg.editable) {
            if (value === '&nbsp;')
                this.label.val('');
            else
                this.label.val(displayedLabel);

            var hasPlaceholder = this.label[0].hasAttribute('placeholder');
            this.updatePlaceholderClass((hasPlaceholder && value === '&nbsp;'));
        }
        else if (this.cfg.alwaysDisplayLabel && this.cfg.label) {
            this.label.text(this.cfg.label);
        }
        else {
            var labelText = this.label.data('placeholder');
            if (labelText == null || labelText == "") {
                labelText = '&nbsp;';
            }

            this.updatePlaceholderClass((value === '&nbsp;' && labelText !== '&nbsp;'));

            if (value === '&nbsp;') {
                if (labelText != '&nbsp;') {
                   this.label.text(labelText);
                } else {
                    this.label.html(labelText);
                }
            }
            else {
                this.label.removeClass('ui-state-disabled');

                var option = null;
                if(this.items) {
                    var selectedItem = this.items.filter('[data-label="' + $.escapeSelector(value) + '"]');
                    option = this.options.eq(this.resolveItemIndex(selectedItem));
                }
                else {
                    option = this.options.filter(':selected');
                }

                if (option && option.data('escape') === false) {
                    this.label.html(displayedLabel);
                } else {
                    this.label.text(displayedLabel);
                }
            }
        }
    },

    /**
     * Selects the option with the given value.
     * @param {string} value Value of the option to select.
     */
    selectValue : function(value) {
        if(!this.items || this.items.length === 0) {
           this.callHandleMethod(null, null); 
        }

        var option = this.options.filter('[value="' + $.escapeSelector(value) + '"]');

        this.selectItem(this.items.eq(option.index()), true);
    },

    /**
     * Finds the element for the currently select option of this select one menu.
     * @return {JQuery} The DOM element that represents the currently selected option.
     */
    getActiveItem: function() {
        return this.items.filter('.ui-state-highlight');
    },

    /**
     * Finds and stores the filter function which is to be used for filtering the options of this select one menu.
     * @private
     */
    setupFilterMatcher: function() {
        this.cfg.filterMatchMode = this.cfg.filterMatchMode||'startsWith';
        this.filterMatchers = {
            'startsWith': this.startsWithFilter
            ,'contains': this.containsFilter
            ,'endsWith': this.endsWithFilter
            ,'custom': this.cfg.filterFunction
        };

        this.filterMatcher = this.filterMatchers[this.cfg.filterMatchMode];
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectOneMenu.FilterFunction` that matches the given option when it starts
     * with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options starts with the filter value, or `false` otherwise.
     */
    startsWithFilter: function(value, filter) {
        return value.indexOf(filter) === 0;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectOneMenu.FilterFunction` that matches the given option when it
     * contains the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the contains the filter value, or `false` otherwise.
     */
    containsFilter: function(value, filter) {
        return value.indexOf(filter) !== -1;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectOneMenu.FilterFunction` that matches the given option when it ends
     * with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options ends with the filter value, or `false` otherwise.
     */
    endsWithFilter: function(value, filter) {
        return value.indexOf(filter, value.length - filter.length) !== -1;
    },

    /**
     * Filters the available options in the overlay panel by the given search value. Note that this does not bring up
     * the overlay panel, use `show` for that.
     * @param {string} value A value against which the available options are matched.
     */
    filter: function(value) {
        this.cfg.initialHeight = this.cfg.initialHeight||this.itemsWrapper.height();
        var filterValue = this.cfg.caseSensitive ? PrimeFaces.trim(value) : PrimeFaces.trim(value).toLowerCase();

        if(filterValue === '') {
            this.items.filter(':hidden').show();
            this.itemsContainer.children('.ui-selectonemenu-item-group').show();
        }
        else {
            var hide = [];
            var show = [];

            for(var i = 0; i < this.options.length; i++) {
                var option = this.options.eq(i),
                itemLabel = this.cfg.caseSensitive ? option.text() : option.text().toLowerCase(),
                item = this.items.eq(i);

                if(item.hasClass('ui-noselection-option')) {
                    hide.push(item);
                }
                else {
                    if(this.filterMatcher(itemLabel, filterValue))
                        show.push(item);
                    else
                        hide.push(item);
                }
            }

            $.each(hide, function(i, o) { o.hide() });
            $.each(show, function(i, o) { o.show() });
            hide = [];
            show = [];

            //Toggle groups
            var groups = this.itemsContainer.children('.ui-selectonemenu-item-group');
            for(var g = 0; g < groups.length; g++) {
                var group = groups.eq(g);

                if(g === (groups.length - 1)) {
                    if(group.nextAll().filter(':visible').length === 0)
                        hide.push(group);
                    else
                        show.push(group);
                }
                else {
                    if(group.nextUntil('.ui-selectonemenu-item-group').filter(':visible').length === 0)
                        hide.push(group);
                    else
                        show.push(group);
                }
            }

            $.each(hide, function(i, o) { o.hide() });
            $.each(show, function(i, o) { o.show() });
        }

        var firstVisibleItem = this.items.filter(':visible:not(.ui-state-disabled):first');
        if(firstVisibleItem.length) {
            this.highlightItem(firstVisibleItem);
        }

        if(this.itemsContainer.height() < this.cfg.initialHeight) {
            this.itemsWrapper.css('height', 'auto');
        }
        else {
            this.itemsWrapper.height(this.cfg.initialHeight);
        }

        this.alignPanel();
    },

    /**
     * Finds the value of the currently selected item, if any.
     * @return {string} The value of the currently selected item. Empty string if none is selected. 
     */
    getSelectedValue: function() {
        return this.input.val();
    },

    /**
     * Finds the label of the currently selected item, if any.
     * @return {string} The label of the currently selected item. Empty string if none is selected. 
     */
    getSelectedLabel: function() {
        return this.options.filter(':selected').text();
    },

    /**
     * Finds the label of the option with the given value.
     * @private
     * @param {string} value The value of a selectable option.
     * @return {string} The label of the option with the given value.
     */
    getLabelToDisplay: function(value) {
        if(this.cfg.labelTemplate && value !== '&nbsp;') {
            return this.cfg.labelTemplate.replace('{0}', value);
        }
        return String(value);
    },

    /**
     * Adjusts the value of the aria attributes for the given selectable option.
     * @private
     * @param {JQuery} item An option for which to set the aria attributes. 
     */
    changeAriaValue: function (item) {
        var itemId = item.attr('id');

        this.focusInput.attr('aria-activedescendant', itemId)
                .attr('aria-describedby', itemId);
        this.itemsContainer.attr('aria-activedescendant', itemId);
    },

    /**
     * Loads the overlay panel with the selectable options, if dynamic mode is enabled.
     * @private
     */
    dynamicPanelLoad: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_dynamicload', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        var $content = $($.parseHTML(content));

                        var $ul = $content.filter('ul');
                        $this.itemsWrapper.empty();
                        $this.itemsWrapper.append($ul);

                        var $select = $content.filter('select');
                        $this.input.replaceWith($select);
                    }
                });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                $this.isDynamicLoaded = true;
                $this.input = $($this.jqId + '_input');
                $this.options = $this.input.children('option');

                $this.renderPanelContentFromHiddenSelect(false);

                $this.initContents();
                $this.bindItemEvents();
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Invokes the given method after making sure that the overlay panel was loaded (in case dynamic mode is enabled).
     * @private
     * @param {(this: PrimeFaces.widget.SelectOneMenu, event: JQuery.TriggeredEvent) => void} handleMethod Callback method to
     * invoke after the dynamic overlay panel was loaded. 
     * @param {JQuery.TriggeredEvent} event An event that is passed to the callback. 
     */
    callHandleMethod: function(handleMethod, event) {
        var $this = this;
        if(this.cfg.dynamic && !this.isDynamicLoaded) {
            this.dynamicPanelLoad();

            var interval = setInterval(function() {
                if($this.isDynamicLoaded) {
                    if (handleMethod) {
                        handleMethod.call($this, event);
                    }

                    clearInterval(interval);
                }
            }, 10);
        }
        else {
            this.renderPanelContentFromHiddenSelect(true);

            if (handleMethod) {
                handleMethod.call(this, event);
            }
        }
    },

    /**
     * Renders panel content based on hidden select.
     * @private
     * @param {boolean} initContentsAndBindItemEvents `true` to call {@link initContents} and {@link bindItemEvents}
     * after rendering, `false` otherwise.
     */
    renderPanelContentFromHiddenSelect: function(initContentsAndBindItemEvents) {
         if (this.cfg.renderPanelContentOnClient && this.itemsWrapper.children().length === 0) {
             var panelContent = '<ul id="' + this.id + '_items" class="ui-selectonemenu-items ui-selectonemenu-list ui-widget-content ui-widget ui-corner-all ui-helper-reset" role="listbox">';
             panelContent += this.renderSelectItems(this.input);
             panelContent += '</ul>';

             this.itemsWrapper.append(panelContent);

             if (initContentsAndBindItemEvents) {
                 this.initContents();
                 this.bindItemEvents();
             }
         }
    },

    /**
     * Renders panel HTML-code for all select items.
     * @private
     * @param {JQuery} parentItem A parent item (select, optgroup) for which to render HTML code.
     * @param {boolean} [isGrouped] Indicated whether the elements of the parent item should be marked as grouped.
     * @return {string} The rendered HTML string.
     */
    renderSelectItems: function(parentItem, isGrouped) {
        var $this = this;
        var content = "";
        isGrouped = isGrouped || false;

        var opts = parentItem.children("option, optgroup");
        opts.each(function(index, element) {
            content += $this.renderSelectItem(element, isGrouped);
        });
        return content;
    },

    /**
     * Renders panel HTML code for one select item (group).
     * @private
     * @param {JQuery} item An option (group) for which to render HTML code.
     * @param {boolean} [isGrouped] Indicates whether the item is part of a group.
     * @return {string} The rendered HTML string.
     */
    renderSelectItem: function(item, isGrouped) {
        var content = "";
        var $item = $(item);
        var label;
        var title = $item.data("title");
        var escape = $item.data("escape");
        var cssClass;

        if (item.tagName === "OPTGROUP") {
            label = $item.attr("label");
            if (escape) {
                label = $("<div>").text(label).html();
            }
            cssClass = "ui-selectonemenu-item-group ui-corner-all";
        }
        else { //OPTION
            if (escape) {
                label = $item.html();
                if ($item.text() === "&nbsp;") {
                    label = $item.text();
                }
            }
            else {
                label = $item.text();
            }
            cssClass = "ui-selectonemenu-item ui-selectonemenu-list-item ui-corner-all";
            if (isGrouped) {
                cssClass += " ui-selectonemenu-item-group-children"
            }
        }

        var dataLabel = label.replace(/(<([^>]+)>)/gi, "");
        if ($item.data("noselection-option")) {
            cssClass += " ui-noselection-option";
        }

        content += '<li class="' + cssClass + '" tabindex="-1" role="option"';
        if (title) {
            content += ' title="' + title + '"';
        }
        if ($item.is(':disabled')) {
            content += ' disabled';
        }
        content += ' data-label="' + dataLabel + '"';
        content += '>';
        content += label;
        content += '</li>';

        if (item.tagName === "OPTGROUP") {
            content += this.renderSelectItems($item, true);
        }

        return content;
    },


    /**
     * Updates the style class of the label that indicates the currently selected item.
     * @param {boolean} add `true` if a placeholder should be displayed, or `false` otherwise. 
     */
    updatePlaceholderClass: function(add) {
        if (add) {
            this.label.addClass('ui-selectonemenu-label-placeholder');
        }
        else {
            this.label.removeClass('ui-selectonemenu-label-placeholder');
        }
    }

});

/**
 * __PrimeFaces SelectOneRadio Widget__
 * 
 * SelectOneRadio is an extended version of the standard SelectOneRadio with theme integration.
 * 
 * @prop {JQuery} originalInputs The DOM elements for the hidden radio input fields of type checkbox storing the value
 * of this widget.
 * @prop {JQuery} enabledInputs The (cloned) DOM elements for the non-disabled hidden input fields of type radio storing
 * the value of this widget. 
 * @prop {JQuery} inputs The (cloned) DOM elements for the hidden input fields of type radio storing the value of this
 * widget.
 * @prop {JQuery} outputs The DOM elements for the radio icons shown on the UI.
 * @prop {JQuery} checkedRadio The DOM elements for the active radio icons shown on the UI .
 * @prop {JQuery} labels The DOM elements for the label texts of each radio button.
 * 
 * @interface {PrimeFaces.widget.SelectOneRadioCfg} cfg The configuration for the {@link  SelectOneRadio| SelectOneRadio 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.custom Whether a custom layout is enabled.
 * @prop {boolean} cfg.unselectable Unselectable mode when true clicking a radio again will clear the selection.
 */
PrimeFaces.widget.SelectOneRadio = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        //custom layout
        if(this.cfg.custom) {
            this.originalInputs = this.jq.find(':radio');
            this.inputs = $('input:radio[name="' + this.id + '"].ui-radio-clone');
            this.outputs = this.inputs.parent().next('.ui-radiobutton-box');
            this.labels = $();

            //labels
            for(var i=0; i < this.outputs.length; i++) {
                this.labels = this.labels.add('label[for="' + this.outputs.eq(i).parent().attr('id') + '"]');
            }

            //update radio state
            for(var i = 0; i < this.inputs.length; i++) {
                var input = this.inputs.eq(i),
                itemindex = input.data('itemindex'),
                original = this.originalInputs.eq(itemindex);

                input.val(original.val());

                if(original.is(':checked')) {
                    input.prop('checked', true).parent().next().addClass('ui-state-active').children('.ui-radiobutton-icon')
                            .addClass('ui-icon-bullet').removeClass('ui-icon-blank');
                }

                if(original.is(':disabled')) {
                    this.disable(i);
                }
            }

            //pfs metadata
            this.originalInputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        }
        //regular layout
        else {
            this.outputs = this.jq.find('.ui-radiobutton-box');
            this.inputs = this.jq.find(':radio');
            this.labels = this.jq.find('label');

            //pfs metadata
            this.inputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        }

        this.enabledInputs = this.inputs.filter(':not(:disabled)');
        this.checkedRadio = this.outputs.filter('.ui-state-active');

        this.bindEvents();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        if(this.cfg.custom) {
            for(var i = 0; i < this.inputs.length; i++) {
                var input = this.inputs.eq(i);

                this.enable(i);
                input.prop('checked', false).parent().next().removeClass('ui-state-active').children('.ui-radiobutton-icon')
                            .removeClass('ui-icon-bullet').addClass('ui-icon-blank');
            }
        }

        this.init(cfg);
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.outputs.filter(':not(.ui-state-disabled)').on('mouseenter.selectOneRadio', function() {
            $(this).addClass('ui-state-hover');
        })
        .on('mouseleave.selectOneRadio', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click.selectOneRadio', function(e) {
            var radio = $(this),
            input = radio.prev().children(':radio');

            if(!radio.hasClass('ui-state-active')) {
                $this.unselect($this.checkedRadio);
                $this.select(radio);
                $this.fireClickEvent(input, e);
                input.trigger('change');
            }
            else {
                if ($this.cfg.unselectable) {
                    $this.unselect($this.checkedRadio);
                }
                $this.fireClickEvent(input, e);
            }

            input.trigger('focus.selectOneRadio');

            // Github issue #4467
            e.stopPropagation();
            e.preventDefault();
        });

        this.labels.filter(':not(.ui-state-disabled)').on('click.selectOneRadio', function(e) {
            var target = $(PrimeFaces.escapeClientId($(this).attr('for'))),
            radio = null;

            //checks if target is input or not(custom labels)
            if(target.is(':input'))
                radio = target.parent().next();
            else
                radio = target.children('.ui-radiobutton-box'); //custom layout

            radio.trigger('click.selectOneRadio');

            e.preventDefault();
        });

        this.enabledInputs.on('focus.selectOneRadio', function() {
            var input = $(this),
            radio = input.parent().next();

            radio.addClass('ui-state-focus');
        })
        .on('blur.selectOneRadio', function() {
            var input = $(this),
            radio = input.parent().next();

            radio.removeClass('ui-state-focus');
        })
        .on('keydown.selectOneRadio', function(e) {
            var input = $(this),
            currentRadio = input.parent().next(),
            index = $this.enabledInputs.index(input),
            size = $this.enabledInputs.length,
            keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                case keyCode.LEFT:
                    var prevRadioInput = (index === 0) ? $this.enabledInputs.eq((size - 1)) : $this.enabledInputs.eq(--index),
                    prevRadio = prevRadioInput.parent().next();

                    input.trigger("blur");
                    $this.unselect(currentRadio);
                    $this.select(prevRadio);
                    prevRadioInput.trigger('focus').trigger('change');
                    e.preventDefault();
                break;

                case keyCode.DOWN:
                case keyCode.RIGHT:
                    var nextRadioInput = (index === (size - 1)) ? $this.enabledInputs.eq(0) : $this.enabledInputs.eq(++index),
                    nextRadio = nextRadioInput.parent().next();

                    input.trigger("blur");
                    $this.unselect(currentRadio);
                    $this.select(nextRadio);
                    nextRadioInput.trigger('focus').trigger('change');
                    e.preventDefault();
                break;

                case keyCode.SPACE:
                    if(!input.prop('checked')) {
                        $this.select(currentRadio);
                        input.trigger('focus').trigger('change');
                    }

                    e.preventDefault();
                break;
            }
        });
    },

    /**
     * Unselects the given radio button option.
     * @param {JQuery} radio A radio button of this widget to unselect.
     */
    unselect: function(radio) {
        var radioInput = radio.prev().children(':radio');
        radioInput.prop('checked', false);
        radio.removeClass('ui-state-active').children('.ui-radiobutton-icon').removeClass('ui-icon-bullet').addClass('ui-icon-blank');

        if (this.cfg.custom) {
            var itemindex = radioInput.data('itemindex');
            this.originalInputs.eq(itemindex).prop('checked', false);
        }
    },

    /**
     * Selects the given radio button option. If another radio button option is selected already, it will be unselected.
     * @param {JQuery} radio A radio button of this widget to select.
     */
    select: function(radio) {
        var radioInput = radio.prev().children(':radio');
        this.checkedRadio = radio;
        radio.addClass('ui-state-active').children('.ui-radiobutton-icon').addClass('ui-icon-bullet').removeClass('ui-icon-blank');
        radioInput.prop('checked', true);

        if (this.cfg.custom) {
            var itemindex = radioInput.data('itemindex');
            this.originalInputs.eq(itemindex).prop('checked', true);
        }
    },

    /**
     * Removes some of the event listeners added by `bindEvents`. Called when this widget is disabled.
     * @private
     * @param {JQuery} input Radio input element for which to remove the listeners. 
     */
    unbindEvents: function(input) {
        if(input) {
            input.off();
            input.parent().nextAll('.ui-radiobutton-box').off();
            this.labels.filter("label[for='" + input.attr('id') + "']").off();
        }
        else {
            this.inputs.off();
            this.labels.off();
            this.outputs.off();
        }
    },

    /**
     * Disables a given radio button option of this widget.
     * @param {number} index Index of the radio button option to disable. 
     */
    disable: function(index) {
        if(index == null) {
            this.inputs.attr('disabled', 'disabled');
            this.labels.addClass('ui-state-disabled');
            this.outputs.addClass('ui-state-disabled');
            this.unbindEvents();
        }
        else {
            var input = this.inputs.eq(index),
                label = this.labels.filter("label[for='" + input.attr('id') + "']");
            input.attr('disabled', 'disabled').parent().nextAll('.ui-radiobutton-box').addClass('ui-state-disabled');
            label.addClass('ui-state-disabled');
            this.unbindEvents(input);
        }

    },

    /**
     * Enables a given radio button option of this widget.
     * @param {number} index Index of the radio button option to enable. 
     */
    enable: function(index) {
        if(index == null) {
            this.inputs.removeAttr('disabled');
            this.labels.removeClass('ui-state-disabled');
            this.outputs.removeClass('ui-state-disabled');
        }
        else {
            var input = this.inputs.eq(index),
                label = this.labels.filter("label[for='" + input.attr('id') + "']");
            input.removeAttr('disabled').parent().nextAll('.ui-radiobutton-box').removeClass('ui-state-disabled');
            label.removeClass('ui-state-disabled');
        }
        this.bindEvents();
    },

    /**
     * Calls the behavior for when a radio button options was clicked.
     * @private
     * @param {JQuery} input Radio button input that was clicked.
     * @param {JQuery.TriggeredEvent} event (Click) event that was triggered.
     */
    fireClickEvent: function(input, event) {
        var userOnClick = input.prop('onclick');
        if (userOnClick) {
            userOnClick.call(this, event);
        }
    }

});

/**
 * __PrimeFaces SelectBooleanCheckbox Widget__
 * 
 * SelectBooleanCheckbox is an extended version of the standard checkbox with theme integration.
 * 
 * @prop {JQuery} input The DOM element for the hidden input field storing the current value of this widget.
 * @prop {JQuery} box The DOM element for the box with the checkbox.
 * @prop {JQuery} icon The DOM element for the checked or unchecked checkbox icon.
 * @prop {JQuery} itemLabel The DOM element for the label of the checkbox.
 * @prop {boolean} disabled Whether this checkbox is disabled.
 * 
 * @interface {PrimeFaces.widget.SelectBooleanCheckboxCfg} cfg The configuration for the {@link  SelectBooleanCheckbox| SelectBooleanCheckbox 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
 */
PrimeFaces.widget.SelectBooleanCheckbox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.input = $(this.jqId + '_input');
        this.box = this.jq.find('.ui-chkbox-box');
        this.icon = this.box.children('.ui-chkbox-icon');
        this.itemLabel = this.jq.find('.ui-chkbox-label');
        this.disabled = this.input.is(':disabled');

        var $this = this;

        //bind events if not disabled
        if(!this.disabled) {
            this.box.on('mouseenter.selectBooleanCheckbox', function() {
                $this.box.addClass('ui-state-hover');
            })
            .on('mouseleave.selectBooleanCheckbox', function() {
                $this.box.removeClass('ui-state-hover');
            })
            .on('click.selectBooleanCheckbox', function() {
                $this.input.trigger('click').trigger('focus.selectBooleanCheckbox');
            });

            this.input.on('focus.selectBooleanCheckbox', function() {
                $this.box.addClass('ui-state-focus');
            })
            .on('blur.selectBooleanCheckbox', function() {
                $this.box.removeClass('ui-state-focus');
            })
            .on('change.selectBooleanCheckbox', function(e) {
                if($this.isChecked()) {
                    $this.input.prop('checked', true).attr('aria-checked', true);
                    $this.box.addClass('ui-state-active').children('.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
                }
                else {
                    $this.input.prop('checked', false).attr('aria-checked', false);
                    $this.box.removeClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
                }
            });

            //toggle state on label click
            this.itemLabel.on("click", function() {
                $this.toggle();
                $this.input.trigger('focus');
            });
        }

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Checks this checkbox if it is currently unchecked, or unchecks it otherwise.
     */
    toggle: function() {
        if(this.isChecked())
            this.uncheck();
        else
            this.check();
    },

    /**
     * Checks whether this checkbox is currently checked.
     * @return {boolean} `true` if this checkbox is checked, or `false` otherwise.
     */
    isChecked: function() {
        return this.input.prop('checked');
    },

    /**
     * Checks this checkbox, if it is not checked already .
     */
    check: function() {
        if(!this.isChecked()) {
            this.input.prop('checked', true).trigger('change');
            this.input.attr('aria-checked', true);
            this.box.addClass('ui-state-active').children('.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
        }
    },

    /**
     * Unchecks this checkbox, if it is not unchecked already .
     */
    uncheck: function() {
        if(this.isChecked()) {
            this.input.prop('checked', false).trigger('change');
            this.input.attr('aria-checked', false);
            this.box.removeClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
        }
    }

});

/**
 * __PrimeFaces SelectManyCheckbox Widget__
 * 
 * SelectManyCheckbox is an extended version of the standard SelectManyCheckbox.
 * 
 * @prop {JQuery} originalInputs The DOM elements for the hidden input fields of type checkbox storing the value of
 * this widget.
 * @prop {JQuery} enabledInputs The (cloned) DOM elements for the non-disabled hidden input fields of type checkbox
 * storing the value of this widget. 
 * @prop {JQuery} inputs The (cloned) DOM elements for the hidden input fields of type checkbox storing the value of
 * this widget.
 * @prop {JQuery} outputs The DOM elements for the checkbox icons shown on the UI.
 * 
 * @interface {PrimeFaces.widget.SelectManyCheckboxCfg} cfg The configuration for the {@link  SelectManyCheckbox| SelectManyCheckbox 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.custom Whether a custom HTML snippet needs to be used for the individual select items.
 */
PrimeFaces.widget.SelectManyCheckbox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        if(this.cfg.custom) {
            this.originalInputs = this.jq.find(':checkbox');
            this.inputs = $('input:checkbox[name="' + this.id + '"].ui-chkbox-clone');
            this.outputs = this.inputs.parent().next('.ui-chkbox-box');

            //update checkbox state
            for(var i = 0; i < this.inputs.length; i++) {
                var input = this.inputs.eq(i),
                itemindex = input.data('itemindex'),
                original = this.originalInputs.eq(itemindex);

                input.val(original.val());

                if(original.is(':checked')) {
                    input.prop('checked', true).parent().next().addClass('ui-state-active').children('.ui-chkbox-icon')
                            .addClass('ui-icon-check').removeClass('ui-icon-blank');
                }
            }
        }
        else {
            this.outputs = this.jq.find('.ui-chkbox-box:not(.ui-state-disabled)');
            this.inputs = this.jq.find(':checkbox:not(:disabled)');
        }

        this.enabledInputs = this.inputs.filter(':not(:disabled)');

        this.bindEvents();

        //pfs metadata
        this.inputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        this.outputs.filter(':not(.ui-state-disabled)').on('mouseenter', function() {
            $(this).addClass('ui-state-hover');
        })
        .on('mouseleave', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click', function() {
            var checkbox = $(this),
            input = checkbox.prev().children(':checkbox');

            input.trigger('click').trigger('focus');

            if($.browser.msie && parseInt($.browser.version) < 9) {
                input.trigger('change');
            }
        });

        //delegate focus-blur-change states
        this.enabledInputs.on('focus', function() {
            var input = $(this),
            checkbox = input.parent().next();

            checkbox.addClass('ui-state-focus');
        })
        .on('blur', function() {
            var input = $(this),
            checkbox = input.parent().next();

            checkbox.removeClass('ui-state-focus');
        })
        .on('change', function(e) {
            var input = $(this),
            checkbox = input.parent().next(),
            disabled = input.is(':disabled');

            if(disabled) {
                return;
            }

            if(input.is(':checked')) {
                checkbox.children('.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');

                checkbox.addClass('ui-state-active');
            }
            else {
                checkbox.removeClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
            }
        });
    }

});

/**
 * __PrimeFaces SelectListbox Widget__
 * 
 * Base class for the `SelectManyMenu` and `SelectOneListBox` widgets. Contains some common functionality such as
 * filtering and working with SELECT and OPTION elements.
 * 
 * @typedef {"startsWith" |  "contains" |  "endsWith" | "custom"} PrimeFaces.widget.SelectListbox.FilterMatchMode
 * Available modes for filtering the options of a select list box. When `custom` is set, a `filterFunction` must be
 * specified.
 * 
 * @typedef PrimeFaces.widget.SelectListbox.FilterFunction A function for filtering the options of a select list box.
 * @param {string} PrimeFaces.widget.SelectListbox.FilterFunction.itemLabel The label of the currently selected text.
 * @param {string} PrimeFaces.widget.SelectListbox.FilterFunction.filterValue The value to search for.
 * @return {boolean} PrimeFaces.widget.SelectListbox.FilterFunction `true` if the item label matches the filter value,
 * or `false` otherwise.
 * 
 * @prop {JQuery} allItems All available items, including disabled options. These are not form elements, but the DOM
 * elements presented to the user.
 * @prop {PrimeFaces.widget.SelectListbox.FilterFunction} filterMatcher The filter that was selected and is currently
 * used.
 * @prop {Record<PrimeFaces.widget.SelectListbox.FilterMatchMode, PrimeFaces.widget.SelectListbox.FilterFunction>} filterMatchers
 * Map between the available filter types and the filter implementation.
 * @prop {JQuery} input The hidden INPUT or SELECT element.
 * @prop {JQuery} items All available items, excluding disabled options. These are not form elements, but the DOM
 * elements presented to the user.
 * @prop {JQuery} listContainer Container of the list element.
 * @prop {JQuery} listElement The element that contains the available items.
 * @prop {JQuery<HTMLOptionElement>} options A list of the available OPTION elements of the hidden SELECT element.
 * 
 * @interface {PrimeFaces.widget.SelectListboxCfg} cfg The configuration for the {@link  SelectListbox| SelectListbox 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.caseSensitive `true` if filtering is case-sensitive, `false` otherwise.
 * @prop {boolean} cfg.filter `true` if the options can be filtered, or `false` otherwise.
 * @prop {PrimeFaces.widget.SelectListbox.FilterFunction} cfg.filterFunction A custom filter function that is used when
 * `filterMatchMode` is set to `custom`.
 * @prop {PrimeFaces.widget.SelectListbox.FilterMatchMode} cfg.filterMatchMode Mode of the filter. When set to `custom`
 *  a `filterFunction` must be specified.
 */
PrimeFaces.widget.SelectListbox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.input = $(this.jqId + '_input'),
        this.listContainer = this.jq.children('.ui-selectlistbox-listcontainer');
        this.listElement = this.listContainer.children('.ui-selectlistbox-list');
        this.options = $(this.input).children('option');
        this.allItems = this.listElement.find('.ui-selectlistbox-item');
        this.items = this.allItems.filter(':not(.ui-state-disabled)');

        //scroll to selected
        var selected = this.options.filter(':selected:not(:disabled)');
        if(selected.length) {
            PrimeFaces.scrollInView(this.listContainer, this.items.eq(selected.eq(0).index()));
        }

        this.bindEvents();

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Sets up all event listeners for this widget instance.
     * @protected
     */
    bindEvents: function() {
        var $this = this;

        //items
        this.items.on('mouseover.selectListbox', function() {
            var item = $(this);
            if(!item.hasClass('ui-state-highlight')) {
                item.addClass('ui-state-hover');
            }
        })
        .on('mouseout.selectListbox', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('dblclick.selectListbox', function(e) {
            $this.input.trigger('dblclick');

            PrimeFaces.clearSelection();
            e.preventDefault();
        });

        //input
        this.input.on('focus.selectListbox', function() {
            $this.jq.addClass('ui-state-focus');
        }).on('blur.selectListbox', function() {
            $this.jq.removeClass('ui-state-focus');
        });

        if(this.cfg.filter) {
            this.filterInput = this.jq.find('> div.ui-selectlistbox-filter-container > input.ui-selectlistbox-filter');
            PrimeFaces.skinInput(this.filterInput);
            this.filterInput.on('keyup.selectListbox', function(e) {
                $this.filter(this.value);
            });

            this.setupFilterMatcher();
        }
    },

    /**
     * Unselects all items that are currently selected.
     */
    unselectAll: function() {
        this.items.removeClass('ui-state-highlight ui-state-hover');
        this.options.filter(':selected').prop('selected', false);
    },

    /**
     * Select the given item as the currently selected option. Does not unselect other items that are already selected.
     * @param {JQuery} item An OPTION element to set as the selected element.
     */
    selectItem: function(item) {
        item.addClass('ui-state-highlight').removeClass('ui-state-hover');
        this.options.eq(item.index()).prop('selected', true);
    },

    /**
     * Unselect the given items. Does not change other selected items. 
     * @param {JQuery} item Item to unselect.
     */
    unselectItem: function(item) {
        item.removeClass('ui-state-highlight');
        this.options.eq(item.index()).prop('selected', false);
    },

    /**
     * Finds and stores the filter function which is to be used for filtering the options of this select list box.
     * @private
     */
    setupFilterMatcher: function() {
        this.cfg.filterMatchMode = this.cfg.filterMatchMode||'startsWith';
        this.filterMatchers = {
            'startsWith': this.startsWithFilter
            ,'contains': this.containsFilter
            ,'endsWith': this.endsWithFilter
            ,'custom': this.cfg.filterFunction
        };

        this.filterMatcher = this.filterMatchers[this.cfg.filterMatchMode];
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectListbox.FilterFunction` that matches the given option when it starts
     * with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options starts with the filter value, or `false` otherwise.
     */
    startsWithFilter: function(value, filter) {
        return value.indexOf(filter) === 0;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectListbox.FilterFunction` that matches the given option when it
     * contains the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the contains the filter value, or `false` otherwise.
     */
    containsFilter: function(value, filter) {
        return value.indexOf(filter) !== -1;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectListbox.FilterFunction` that matches the given option when it ends
     * with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options ends with the filter value, or `false` otherwise.
     */
    endsWithFilter: function(value, filter) {
        return value.indexOf(filter, value.length - filter.length) !== -1;
    },

    /**
     * Filters the options of this select list box by the given search value.
     * @param {string} value Current value of the filter.
     */
    filter: function(value) {
        var filterValue = this.cfg.caseSensitive ? PrimeFaces.trim(value) : PrimeFaces.trim(value).toLowerCase();

        if(filterValue === '') {
            this.items.filter(':hidden').show();
        }
        else {
            for(var i = 0; i < this.options.length; i++) {
                var option = this.options.eq(i),
                itemLabel = this.cfg.caseSensitive ? option.text() : option.text().toLowerCase(),
                item = this.items.eq(i);

                if(this.filterMatcher(itemLabel, filterValue))
                    item.show();
                else
                    item.hide();
            }
        }
    }
});

/**
 * __PrimeFaces SelectOneListbox Widget__
 * 
 * SelectOneListbox is an extended version of the standard selectOneListbox component.
 * 
 * @prop {JQuery} focusedItem The DOM element for the button select item currently focused.
 * @prop {JQuery} items The DOM element for the select items the user can select.
 * @prop {JQuery} input The DOM element for the hidden input field storing the selected item.
 * 
 * @interface {PrimeFaces.widget.SelectOneListboxCfg} cfg The configuration for the {@link  SelectOneListbox| SelectOneListbox 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.SelectListboxCfg} cfg
 * 
 * @prop {boolean} cfg.disabled Whether this widget is currently disabled.
 */
PrimeFaces.widget.SelectOneListbox = PrimeFaces.widget.SelectListbox.extend({

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindEvents: function() {
        this._super();
        var $this = this;

        if(!this.cfg.disabled) {
            this.focusedItem = null;
            
            this.items.on('click.selectListbox', function(e) {
                var item = $(this),
                selectedItem = $this.items.filter('.ui-state-highlight');

                if(item.index() !== selectedItem.index()) {
                    if(selectedItem.length) {
                        $this.unselectItem(selectedItem);
                    }

                    $this.selectItem(item);
                    $this.input.trigger('change');
                }
                
                /* For keyboard navigation */
                $this.removeOutline();
                $this.focusedItem = item;
                $this.input.trigger('focus');

                $this.input.trigger('click');

                PrimeFaces.clearSelection();
                e.preventDefault();
            });
        }
        
        this.bindKeyEvents();
    },
    
    /**
     * Sets up the event listeners for keyboard related events.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        this.input.off('focus.selectListbox blur.selectListbox keydown.selectListbox').on('focus.selectListbox', function(e) {
            $this.jq.addClass('ui-state-focus');
            
            var activeItem = $this.focusedItem||$this.items.filter('.ui-state-highlight:visible:first');
            if(activeItem.length) {
                $this.focusedItem = activeItem;
            }
            else {
                $this.focusedItem = $this.items.filter(':visible:first');
            }
            
            setTimeout(function() {
                if($this.focusedItem) {
                    PrimeFaces.scrollInView($this.listContainer, $this.focusedItem);
                    $this.focusedItem.addClass('ui-listbox-outline');
                }
            }, 100);
        })
        .on('blur.selectListbox', function() {
            $this.jq.removeClass('ui-state-focus');
            $this.removeOutline();
            $this.focusedItem = null;
        })
        .on('keydown.selectListbox', function(e) {
            if(!$this.focusedItem) {
                return;
            }

            var keyCode = $.ui.keyCode,
                key = e.which;

            switch(key) {
                case keyCode.UP:
                    if(!$this.focusedItem.hasClass('ui-state-highlight')) {
                        $this.focusedItem.trigger('click.selectListbox');
                    }
                    else {
                        var prevItem = $this.focusedItem.prevAll('.ui-selectlistbox-item:visible:first');
                        if(prevItem.length) {
                            prevItem.trigger('click.selectListbox');

                            PrimeFaces.scrollInView($this.listContainer, $this.focusedItem);
                        }
                    }
                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    if(!$this.focusedItem.hasClass('ui-state-highlight')) {
                        $this.focusedItem.trigger('click.selectListbox');
                    }
                    else {
                        var nextItem = $this.focusedItem.nextAll('.ui-selectlistbox-item:visible:first');
                        if(nextItem.length) {
                            nextItem.trigger('click.selectListbox');

                            PrimeFaces.scrollInView($this.listContainer, $this.focusedItem);
                        }
                    }
                    e.preventDefault();
                break;
            };
        });

    },
    
    /**
     * Removes the outline around the listbox with the select options.
     * @private
     */
    removeOutline: function() {
        if(this.focusedItem && this.focusedItem.hasClass('ui-listbox-outline')) {
            this.focusedItem.removeClass('ui-listbox-outline');
        }
    }
});
/**
 * __PrimeFaces SelectManyMenu Widget__
 * 
 * SelectManyMenu is an extended version of the standard SelectManyMenu.
 * 
 * @interface {PrimeFaces.widget.SelectManyMenuCfg} cfg The configuration for the {@link  SelectManyMenu| SelectManyMenu 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.SelectListboxCfg} cfg
 * 
 * @prop {boolean} cfg.disabled Whether the select many menu is initially disabled.
 * @prop {boolean} cfg.metaKeySelection Whether the meta key (`SHIFT` or `CTRL`) must be held down to select multiple
 * items.
 * @prop {boolean} cfg.showCheckbox When set to `true`, a checkbox is displayed next to each item.
 */
PrimeFaces.widget.SelectManyMenu = PrimeFaces.widget.SelectListbox.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.cfg.metaKeySelection = this.cfg.metaKeySelection != undefined ? this.cfg.metaKeySelection : true;

        this.allItems.filter('.ui-state-highlight').find('> .ui-chkbox > .ui-chkbox-box').addClass('ui-state-active');
        // Checkbox is inside TD element when using custom content
        this.allItems.filter('.ui-state-highlight').find('> td > .ui-chkbox > .ui-chkbox-box').addClass('ui-state-active');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindEvents: function() {
        this._super();
        var $this = this;

        if(!this.cfg.disabled) {
            this.items.on('click.selectListbox', function(e) {
                //stop propagation
                if($this.checkboxClick) {
                    $this.checkboxClick = false;
                    return;
                }

                var item = $(this),
                selectedItems = $this.items.filter('.ui-state-highlight'),
                metaKey = $this.cfg.metaKeySelection && (e.metaKey||e.ctrlKey);

                if(!e.shiftKey) {
                    if(!metaKey && !$this.cfg.showCheckbox) {
                        $this.unselectAll();
                    }

                    if((metaKey || $this.cfg.showCheckbox) && item.hasClass('ui-state-highlight')) {
                        $this.unselectItem(item);
                    }
                    else {
                        $this.selectItem(item);
                        $this.cursorItem = item;
                    }
                }
                else {
                    //range selection
                    if($this.cursorItem) {
                        $this.unselectAll();

                        var currentItemIndex = item.index(),
                        cursorItemIndex = $this.cursorItem.index(),
                        startIndex = (currentItemIndex > cursorItemIndex) ? cursorItemIndex : currentItemIndex,
                        endIndex = (currentItemIndex > cursorItemIndex) ? (currentItemIndex + 1) : (cursorItemIndex + 1);

                        for(var i = startIndex ; i < endIndex; i++) {
                            var it = $this.allItems.eq(i);

                            if(it.is(':visible') && !it.hasClass('ui-state-disabled')) {
                                $this.selectItem(it);
                            }
                        }
                    }
                    else {
                        $this.selectItem(item);
                        $this.cursorItem = item;
                    }
                }

                $this.input.trigger('change');
                $this.input.trigger('click');
                PrimeFaces.clearSelection();
                e.preventDefault();
            });

            if(this.cfg.showCheckbox) {
                this.checkboxes = this.jq.find('.ui-selectlistbox-item:not(.ui-state-disabled) div.ui-chkbox > div.ui-chkbox-box');

                this.checkboxes.on('mouseenter.selectManyMenu', function(e) {
                    $(this).addClass('ui-state-hover');
                })
                .on('mouseleave.selectManyMenu', function(e) {
                    $(this).removeClass('ui-state-hover');
                })
                .on('click.selectManyMenu', function(e) {
                    $this.checkboxClick = true;

                    var item = $(this).closest('.ui-selectlistbox-item');
                    if(item.hasClass('ui-state-highlight'))
                        $this.unselectItem(item);
                    else
                        $this.selectItem(item);

                    $this.input.trigger('change');
                });
            }
        }
    },

    /**
     * Selects all available items of this select many menu.
     */
    selectAll: function() {
        // ~~~ PERF ~~~
        // See https://github.com/primefaces/primefaces/issues/2089
        //
        // 1) don't use jquery wrappers
        // 2) don't use existing methods like selectItem

        for(var i = 0; i < this.items.length; i++) {
            var item = this.items.eq(i);
            var itemNative = item[0];

            itemNative.classList.add('ui-state-highlight');
            itemNative.classList.remove('ui-state-hover');

            if(this.cfg.showCheckbox) {
                var checkbox = item.find('div.ui-chkbox').children('div.ui-chkbox-box');

                var checkboxNative = checkbox[0];
                checkboxNative.classList.remove('ui-state-hover');
                checkboxNative.classList.add('ui-state-active');

                var checkboxIconNative = checkbox.children('span.ui-chkbox-icon')[0];
                checkboxIconNative.classList.remove('ui-icon-blank');
                checkboxIconNative.classList.add('ui-icon-check');
            }
        }

        for(var i = 0; i < this.options.length; i++) {
            this.options[i].selected = true;
        }
    },

    /**
     * @override
     * @inheritdoc
     */
    unselectAll: function() {
        // ~~~ PERF ~~~
        // See https://github.com/primefaces/primefaces/issues/2089
        //
        // 1) don't use jquery wrappers
        // 2) don't use existing methods like unselectItem

        for(var i = 0; i < this.items.length; i++) {
            var item = this.items.eq(i);
            var itemNative = item[0];

            itemNative.classList.remove('ui-state-highlight');

            if(this.cfg.showCheckbox) {
                var checkbox = item.find('div.ui-chkbox').children('div.ui-chkbox-box');

                var checkboxNative = checkbox[0];
                checkboxNative.classList.remove('ui-state-active');

                var checkboxIconNative = checkbox.children('span.ui-chkbox-icon')[0];
                checkboxIconNative.classList.add('ui-icon-blank');
                checkboxIconNative.classList.remove('ui-icon-check');
            }
        }

        for(var i = 0; i < this.options.length; i++) {
            this.options[i].selected = false;
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} item
     */
    selectItem: function(item) {
        this._super(item);

        if(this.cfg.showCheckbox) {
            this.selectCheckbox(item.find('div.ui-chkbox-box'));
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} item
     */
    unselectItem: function(item) {
        this._super(item);

        if(this.cfg.showCheckbox) {
            this.unselectCheckbox(item.find('div.ui-chkbox-box'));
        }
    },

    /**
     * Select the given checkbox. Does not unselect any other checkboxes that are currently selected.
     * @param {JQuery} chkbox A CHECKBOX element to select.
     */
    selectCheckbox: function(chkbox) {
        chkbox.addClass('ui-state-active').children('span.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
    },

    /**
     * Unselects the given checkbox. Does not modify any other checkboxes.
     * @param {JQuery} chkbox A CHECKBOX element to unselect.
     */
    unselectCheckbox: function(chkbox) {
        chkbox.removeClass('ui-state-active').children('span.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
    }
});

/**
 * __PrimeFaces CascadeSelect Widget__
 * 
 * CascadeSelect CascadeSelect displays a nested structure of options.
 * 
 * @prop {JQuery} contents The DOM element for the content in the available selectable options.
 * @prop {JQuery} input The DOM element for the hidden input with the current value.
 * @prop {JQuery} items The DOM elements for the the available selectable options.
 * @prop {JQuery} itemsWrapper The DOM element for the wrapper with the container with the available selectable
 * options.
 * @prop {JQuery} label The DOM element for the label indicating the currently selected option.
 * @prop {JQuery} panel The DOM element for the overlay panel with the available selectable options.
 * @prop {JQuery} triggers The DOM elements for the buttons that can trigger (hide or show) the overlay panel with the
 * available selectable options.
 * 
 * @interface {PrimeFaces.widget.CascadeSelectCfg} cfg The configuration for the {@link  CascadeSelect| CascadeSelect 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 {string} cfg.appendTo Appends the overlay to the element defined by search expression. Defaults to the document
 * body.
 * @prop {boolean} cfg.disabled If true, disables the component.
 */
PrimeFaces.widget.CascadeSelect = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        
        this.input = $(this.jqId + '_input');
        this.label = this.jq.children('.ui-cascadeselect-label');
        this.triggers = this.jq.children('.ui-cascadeselect-trigger').add(this.label);
        this.panel = $(this.jqId + '_panel');
        this.itemsWrapper = this.panel.children('.ui-cascadeselect-items-wrapper');
        this.items = this.itemsWrapper.find('li.ui-cascadeselect-item');
        this.contents = this.items.children('.ui-cascadeselect-item-content');
        this.cfg.disabled = this.jq.hasClass('ui-state-disabled');
        this.cfg.appendTo = PrimeFaces.utils.resolveAppendTo(this, this.panel);
        
        if (!this.cfg.disabled) {
            this.bindEvents();
        
            PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');
            this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
        }
    },
    
    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;
    
        this.triggers.off('click.cascadeselect').on('click.cascadeselect', function(e) {
            if ($this.panel.is(':hidden')) {
                $this.show();
            }
            else {
                $this.hide();
            }

            $this.input.trigger('focus.cascadeselect');
            e.preventDefault();
        });
    
        this.input.off('focus.cascadeselect blur.cascadeselect keydown.cascadeselect')
            .on('focus.cascadeselect', function() {
                $this.jq.addClass('ui-state-focus');
            })
            .on('blur.cascadeselect', function(){
                $this.jq.removeClass('ui-state-focus');
            })
            .on('keydown.cascadeselect', function(e) {
                var keyCode = $.ui.keyCode,
                key = e.which;
        
                switch(key) {
                    case keyCode.DOWN:
                        if ($this.panel.is(':visible')) {
                            $this.panel.find('.ui-cascadeselect-item:first > .ui-cascadeselect-item-content').focus();
                        }
                        else if (e.altKey) {
                            $this.show();
                        }
                        e.preventDefault();
                        break;
        
                    case keyCode.ESCAPE:
                        if ($this.panel.is(':visible')) {
                            $this.hide();
                            e.preventDefault();
                        }
                        break;
        
                    case keyCode.TAB:
                        $this.hide();
                        break;
        
                    default:
                        break;
                }
            });
    
        this.contents.off('click.cascadeselect keydown.cascadeselect')
            .on('click.cascadeselect', function(e) {
                var item = $(this).parent();
                var subpanel = item.children('.ui-cascadeselect-panel');

                $this.deactivateItems(item);
                item.addClass('ui-cascadeselect-item-active ui-state-highlight');
                
                if (subpanel.length > 0) {
                    var parentPanel = item.closest('.ui-cascadeselect-panel');
                    $this.alignSubPanel(subpanel, parentPanel);
                    subpanel.show();
                }
                else {
                    $this.input.val(item.attr('data-value'));
                    $this.label.text(item.attr('data-label'));
                    $this.callBehavior('itemSelect');
                    $this.hide();
                    e.stopPropagation();
                }
            })
            .on('keydown.cascadeselect', function(e) {
                var item = $(this).parent();
                var keyCode = $.ui.keyCode,
                key = e.which;
        
                switch(key) {
                    case keyCode.DOWN:
                        var nextItem = item.next();
                        if (nextItem) {
                            nextItem.children('.ui-cascadeselect-item-content').focus();
                        }
                        break;
        
                    case keyCode.UP:
                        var prevItem = item.prev();
                        if (prevItem) {
                            prevItem.children('.ui-cascadeselect-item-content').focus();
                        }
                        break;
        
                    case keyCode.RIGHT:
                        if (item.hasClass('ui-cascadeselect-item-group')) {
                            if (item.hasClass('ui-cascadeselect-item-active')) {
                                item.find('> .ui-cascadeselect-panel > .ui-cascadeselect-item:first > .ui-cascadeselect-item-content').focus();
                            }
                            else {
                                item.children('.ui-cascadeselect-item-content').trigger('click.cascadeselect');
                            }
                        }
                        break;
        
                    case keyCode.LEFT:
                        $this.hideGroup(item);
                        $this.hideGroup(item.siblings('.ui-cascadeselect-item-active'));

                        var parentItem = item.parent().closest('.ui-cascadeselect-item');
                        if (parentItem) {
                            parentItem.children('.ui-cascadeselect-item-content').focus();
                        }
                        break;
        
                    case keyCode.ENTER:
                        item.children('.ui-cascadeselect-item-content').trigger('click.cascadeselect');
                        if (!item.hasClass('ui-cascadeselect-item-group')) {
                            $this.input.trigger('focus.cascadeselect');
                        }
                        break;
        
                    default:
                        break;
                }
        
                e.preventDefault();
            });
    },

    /**
     * Removes some event listeners when this widget was disabled.
     * @private
     */
    unbindEvents: function() {
        this.contents.off();
        this.triggers.off();
        this.input.off();
    },

    /**
     * Disables this widget so that the user cannot select any option.
     */
    disable: function() {
        if (!this.cfg.disabled) {
            this.cfg.disabled = true;
            this.jq.addClass('ui-state-disabled');
            this.input.attr('disabled', 'disabled');
            this.unbindEvents();
        }
    },

    /**
     * Enables this widget so that the user can select an option.
     */
    enable: function() {
        if (this.cfg.disabled) {
            this.cfg.disabled = false;
            this.jq.removeClass('ui-state-disabled');
            this.input.removeAttr('disabled');
            this.bindEvents();
        }
    },

    /**
     * Deactivate siblings and active children of an item.
     * @private
     * @param {JQuery} item Cascade select panel element.
     */
    deactivateItems: function(item) {
        var parentItem = item.parent().parent();
        var siblings = item.siblings('.ui-cascadeselect-item-active');

        this.hideGroup(siblings);
        this.hideGroup(siblings.find('.ui-cascadeselect-item-active'));
        
        if (!parentItem.is(this.itemsWrapper)) {
            this.deactivateItems(parentItem);
        }
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.panel,
            function() { return  $this.triggers },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    $this.hide();
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.panel, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Brings up the overlay panel with the available options.
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.panel.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.input.attr('aria-expanded', true);
                    $this.bindPanelEvents();
                }
            });
        }
    },

    /**
     * Hides the panel of a group item.
     * @param {JQuery} item Dom element of the cascadeselect.
     */
    hideGroup: function(item) {
        item.removeClass('ui-cascadeselect-item-active ui-state-highlight').children('.ui-cascadeselect-panel').hide();
    },

    /**
     * Hides the overlay panel with the available options.
     */
    hide: function() {
        if (this.panel.is(':visible') && this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.panel.css('z-index', '');
                    $this.input.attr('aria-expanded', false);
                }
            });
        }
    },

    /**
     * Adjust the width of the overlay panel.
     * @private 
     */
    alignPanelWidth: function() {
        //align panel and container
        if (this.cfg.appendTo) {
            this.panel.css('min-width', this.jq.outerWidth());
        }
    },

    /**
     * Align the overlay panel with the available options.
     * @private
     */
    alignPanel: function() {
        this.alignPanelWidth();

        if (this.panel.parent().is(this.jq)) {
            this.panel.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px',
                'transform-origin': 'center top'
            });
        }
        else {
            this.panel.css({left:'0px', top:'0px', 'transform-origin': 'center top'}).position({
                my: 'left top'
                ,at: 'left bottom'
                ,of: this.jq
                ,collision: 'flipfit'
                ,using: function(pos, directions) {
                    $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                }
            });
        }
    },

    /**
     * Align the sub overlay panel with the available options.
     * @private
     * @param {JQuery} subpanel Sub panel element of the cascade select panel.
     * @param {JQuery} parentPanel Parent panel element of the sub panel element.
     */
    alignSubPanel: function(subpanel, parentPanel) {
        var subitemWrapper = subpanel.children('.ui-cascadeselect-items-wrapper');
        subpanel.css({'display':'block', 'opacity':'0', 'pointer-events': 'none'});
        subitemWrapper.css({'overflow': 'scroll'});

        subpanel.css({left:'0px', top:'0px'}).position({
                my: 'left top'
                ,at: 'right top'
                ,of: parentPanel
                ,collision: 'flipfit'
            });

        subpanel.css({'display':'none', 'opacity':'', 'pointer-events': '', 'z-index': PrimeFaces.nextZindex()});
        subitemWrapper.css({'overflow': ''});
    }
});

/**
 * __PrimeFaces CommandButton Widget__
 * 
 * CommandButton is an extended version of standard commandButton with AJAX and theming.
 * 
 * @interface {PrimeFaces.widget.CommandButtonCfg} cfg The configuration for the {@link  CommandButton| CommandButton 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
 */
PrimeFaces.widget.CommandButton = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        PrimeFaces.skinButton(this.jq);
    },

    /**
     * Disables this button so that the user cannot press the button anymore.
     */
    disable: function() {
        this.jq.removeClass('ui-state-hover ui-state-focus ui-state-active')
                .addClass('ui-state-disabled').attr('disabled', 'disabled');
    },

    /**
     * Enables this button so that the user can press the button.
     */
    enable: function() {
        this.jq.removeClass('ui-state-disabled').removeAttr('disabled');
    }

});
/**
 * __PrimeFaces Button Widget__
 * 
 * Button is an extension to the standard h:button component with skinning capabilities.
 * 
 * @interface {PrimeFaces.widget.ButtonCfg} cfg The configuration for the {@link  Button| Button 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
 */
PrimeFaces.widget.Button = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        PrimeFaces.skinButton(this.jq);
    },

    /**
     * Enables this button so that the user cannot press it.
     */
    disable: function() {
        this.jq.removeClass('ui-state-hover ui-state-focus ui-state-active')
                .addClass('ui-state-disabled').attr('disabled', 'disabled');
    },

    /**
     * Enables this button so that the user can press it.
     */
    enable: function() {
        this.jq.removeClass('ui-state-disabled').removeAttr('disabled');
    }

});

/**
 * __PrimeFaces LinkButton Widget__
 * 
 * LinkButton a simple link, which is styled as a button and integrated with JSF navigation model.
 *
 * @prop {JQuery} link The DOM element for the link that is a child of the button.
 * 
 * @interface {PrimeFaces.widget.LinkButtonCfg} cfg The configuration for the {@link  LinkButton| LinkButton 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
 */
PrimeFaces.widget.LinkButton = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function (cfg) {
        this._super(cfg);
        this.link = this.jq.children('a');

        PrimeFaces.skinButton(this.jq);

        this.bindEvents();
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function () {
        var $this = this;

        if (this.link.length > 0) {
            this.link.off('focus.linkbutton keydown.linkbutton blur.linkbutton')
                .on('focus.linkbutton keydown.linkbutton', function () {
                    $this.jq.addClass('ui-state-focus ui-state-active');
                }).on('blur.linkbutton', function () {
                    $this.jq.removeClass('ui-state-focus ui-state-active');
                });
        }
    },

    /**
     * Disables this link button so that it cannot be clicked.
     */
    disable: function () {
        this.jq.removeClass('ui-state-hover ui-state-focus ui-state-active')
                .addClass('ui-state-disabled').attr('disabled', 'disabled');
    },

    /**
     * Enables this link button so that it can be clicked.
     */
    enable: function () {
        this.jq.removeClass('ui-state-disabled').removeAttr('disabled');
    }

});
/**
 * __PrimeFaces SelecyManyButton Widget__
 * 
 * SelectManyButton is a multi select component using button UI.
 * 
 * @prop {JQuery} buttons The DOM elements for the selectable buttons.
 * @prop {JQuery} inputs The DOM elements for the hidden input fields of type checkbox storing which buttons are
 * selected.
 * 
 * @interface {PrimeFaces.widget.SelectManyButtonCfg} cfg The configuration for the {@link  SelectManyButton| SelectManyButton 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
 */
PrimeFaces.widget.SelectManyButton = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.buttons = this.jq.children('div:not(.ui-state-disabled)');
        this.inputs = this.jq.find(':checkbox:not(:disabled)');
        this.bindEvents();

        //pfs metadata
        this.inputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.buttons.on('mouseover', function() {
            var button = $(this);
            if(!button.hasClass('ui-state-active')) {
                button.addClass('ui-state-hover');
            }
        })
        .on('mouseout', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click', function(e) {
            var button = $(this),
            input = button.children(':checkbox');

            if(button.hasClass('ui-state-active'))
                button.addClass('ui-state-hover');
            else
                button.removeClass('ui-state-hover');
            
            input.trigger('focus').trigger('click');
        });

        /* Keyboard support */
        this.inputs.on('focus', function() {
            var input = $(this),
            button = input.parent();

            button.addClass('ui-state-focus');
        })
        .on('blur', function() {
            var input = $(this),
            button = input.parent();

            button.removeClass('ui-state-focus');
        })
        .on('change', function() {
            var input = $(this),
            button = input.parent();

            if(input.prop('checked'))
                button.addClass('ui-state-active');
            else
                button.removeClass('ui-state-active');
        })
        .on('click', function(e) {
            e.stopPropagation();
        });
    },

    /**
     * Selects the given button option.
     * @param {JQuery} button A button of this widget to select.
     */
    select: function(button) {
        button.children(':checkbox').prop('checked', true).trigger('change');
    },

    /**
     * Unselects the given button option.
     * @param {JQuery} button A button of this widget to unselect.
     */
    unselect: function(button) {
        button.children(':checkbox').prop('checked', false).trigger('change');
    }

});

/**
 * __PrimeFaces SelectOneButton Widget__
 * 
 * SelectOneButton is an input component to do a single select.
 * 
 * @typedef PrimeFaces.widget.SelectOneButton.ChangeCallback Callback that is invoked when the value of this widget has
 * changed. See also {@link SelectOneButtonCfg.change}.
 * @this {PrimeFaces.widget.SelectOneButton} PrimeFaces.widget.SelectOneButton.ChangeCallback 
 * 
 * @prop {JQuery} buttons The DOM element for the button optios the user can select.
 * @prop {JQuery} inputs The DOM element for the hidden input fields storing the value of this widget. 
 * 
 * @interface {PrimeFaces.widget.SelectOneButtonCfg} cfg The configuration for the {@link  SelectOneButton| SelectOneButton 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.unselectable Whether selection can be cleared.
 * @prop {PrimeFaces.widget.SelectOneButton.ChangeCallback} cfg.change Callback that is invoked when the value of this
 * widget has changed.
 */
PrimeFaces.widget.SelectOneButton = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.buttons = this.jq.children('div:not(.ui-state-disabled)');
        this.inputs = this.jq.find(':radio:not(:disabled)');
        this.cfg.unselectable = this.cfg.unselectable === false ? false : true;

        this.bindEvents();

        //pfs metadata
        this.inputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.buttons.on('mouseover', function() {
            var button = $(this);
            button.addClass('ui-state-hover');
        })
        .on('mouseout', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click', function() {
            var button = $(this),
            radio = button.children(':radio');

            if(button.hasClass('ui-state-active') || radio.prop('checked')) {
                $this.unselect(button);
            }
            else {
                $this.select(button);
            }
        });

        /* For keyboard accessibility */
        this.buttons.on('focus.selectOneButton', function(){
            var button = $(this);
            button.addClass('ui-state-focus');
        })
        .on('blur.selectOneButton', function(){
            var button = $(this);
            button.removeClass('ui-state-focus');
        })
        .on('keydown.selectOneButton', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                var button = $(this),
                radio = button.children(':radio');

                if(radio.prop('checked')) {
                    $this.unselect(button);
                }
                else {
                    $this.select(button);
                }
                e.preventDefault();
            }
        });
    },

    /**
     * Selects the given button option. If another button option is selected already, it will be unselected.
     * @param {JQuery} button A button of this widget to select.
     */
    select: function(button) {
        this.buttons.filter('.ui-state-active').removeClass('ui-state-active ui-state-hover').children(':radio').prop('checked', false);

        button.addClass('ui-state-active').children(':radio').prop('checked', true);

        this.triggerChange();
    },

    /**
     * Unselects the given button option.
     * @param {JQuery} button A button of this widget to unselect.
     */
    unselect: function(button) {
        if(this.cfg.unselectable) {
            button.removeClass('ui-state-active ui-state-hover').children(':radio').prop('checked', false).change();

            this.triggerChange();
        }
    },

    /**
     * Trigger the change behavior when the value of this widget has changed.
     * @private
     */
    triggerChange: function() {
        if(this.cfg.change) {
            this.cfg.change.call(this);
        }

        this.callBehavior('change');
    },

    /**
     * Disables this input components so that the user cannot select an option anymore.
     */
    disable: function() {
        this.buttons.removeClass('ui-state-hover ui-state-focus ui-state-active')
                .addClass('ui-state-disabled').attr('disabled', 'disabled');
    },

    /**
     * Enables this input components so that the user can select an option.
     */
    enable: function() {
        this.buttons.removeClass('ui-state-disabled').removeAttr('disabled');
    }

});

/**
 * __PrimeFaces SelectBooleanButton Widget__
 * 
 * SelectBooleanButton is used to select a binary decision with a toggle button.
 * 
 * @prop {JQuery} icon The DOM element for the icon with the button.
 * @prop {JQuery} input The DOM element for the hidden input field storing the value of this widget.
 * @prop {boolean} disabled Whether this button is disabled.
 * 
 * @interface {PrimeFaces.widget.SelectBooleanButtonCfg} cfg The configuration for the {@link  SelectBooleanButton| SelectBooleanButton 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 {string} cfg.onLabel Label to display when button is selected.
 * @prop {string} cfg.onIcon Icon to display when button is selected.
 * @prop {string} cfg.offLabel Label to display when button is unselected.
 * @prop {string} cfg.offIcon Icon to display when button is unselected.
 */
PrimeFaces.widget.SelectBooleanButton = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.input = $(this.jqId + '_input');
        this.disabled = this.input.is(':disabled');
        this.icon = this.jq.children('.ui-button-icon-left');
        var $this = this;

        //bind events if not disabled
        if(!this.disabled) {
            this.jq.on('mouseover', function() {
                if(!$this.jq.hasClass('ui-state-active')) {
                    $this.jq.addClass('ui-state-hover');
                }
            }).on('mouseout', function() {
                $this.jq.removeClass('ui-state-hover');
            })
            .on('click', function() {
                $this.toggle();
                $this.input.trigger('focus');
            });
        }

        this.input.on('focus', function() {
            $this.jq.addClass('ui-state-focus');
        })
        .on('blur', function() {
            $this.jq.removeClass('ui-state-focus');
        })
        .on('keydown', function(e) {
            var keyCode = $.ui.keyCode;
            if(e.which === keyCode.SPACE) {
                e.preventDefault();
            }
        })
        .on('keyup', function(e) {
            var keyCode = $.ui.keyCode;
            if(e.which === keyCode.SPACE) {
                $this.toggle();

                e.preventDefault();
            }
        });

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Toggles the state of this button, i.e. turning it on if it is off and vice-versa. Corresponds to checking or
     * unchecking the underlying checkbox.
     */
    toggle: function() {
        if(!this.disabled) {
            if(this.input.prop('checked'))
                this.uncheck();
            else
                this.check();
        }
    },

    /**
     * Turns this button to its on state, which corresponds to checking the underlying checkbox.
     */
    check: function() {
        if(!this.disabled) {
            this.input.prop('checked', true);
            this.jq.addClass('ui-state-active').children('.ui-button-text').text(this.cfg.onLabel);

            if(this.icon.length > 0) {
                this.icon.removeClass(this.cfg.offIcon).addClass(this.cfg.onIcon);
            }

            this.input.trigger('change');
        }
    },

    /**
     * Turns this button to its off state, which corresponds to unchecking the underlying checkbox.
     */
    uncheck: function() {
        if(!this.disabled) {
            this.input.prop('checked', false);
            this.jq.removeClass('ui-state-active').children('.ui-button-text').text(this.cfg.offLabel);

            if(this.icon.length > 0) {
                this.icon.removeClass(this.cfg.onIcon).addClass(this.cfg.offIcon);
            }

            this.input.trigger('change');
        }
    }

});

/**
 * __PrimeFaces SelectCheckboxMenu Widget__
 * 
 * SelectCheckboxMenu is a multi select component that displays options in an overlay.
 * 
 * @typedef {"startsWith" |  "contains" |  "endsWith" | "custom"} PrimeFaces.widget.SelectCheckboxMenu.FilterMatchMode
 * Available modes for filtering the options of a select list box. When `custom` is set, a `filterFunction` must be
 * specified.
 * 
 * @typedef PrimeFaces.widget.SelectCheckboxMenu.FilterFunction A function for filtering the options of a select list
 * box.
 * @param {string} PrimeFaces.widget.SelectCheckboxMenu.FilterFunction.itemLabel The label of the currently selected
 * text.
 * @param {string} PrimeFaces.widget.SelectCheckboxMenu.FilterFunction.filterValue The value to search for.
 * @return {boolean} PrimeFaces.widget.SelectCheckboxMenu.FilterFunction `true` if the item label matches the filter
 * value, or `false` otherwise.
 * 
 * @typedef PrimeFaces.widget.SelectCheckboxMenu.OnChangeCallback Callback that is invoked when a checkbox option was
 * checked or unchecked. See also {@link SelectCheckboxMenuCfg.onChange}.
 * @this {PrimeFaces.widget.SelectCheckboxMenu} PrimeFaces.widget.SelectCheckboxMenu.OnChangeCallback 
 * 
 * @typedef PrimeFaces.widget.SelectCheckboxMenu.OnHideCallback Callback that is invoked when the overlay panel is
 * brought up. See also {@link SelectCheckboxMenuCfg.onHide}.
 * @this {PrimeFaces.widget.SelectCheckboxMenu} PrimeFaces.widget.SelectCheckboxMenu.OnHideCallback 
 * 
 * @typedef PrimeFaces.widget.SelectCheckboxMenu.OnShowCallback Callback that is invoked when the overlay panel is
 * hidden. See also {@link SelectCheckboxMenuCfg.onShow}.
 * @this {PrimeFaces.widget.SelectCheckboxMenu} PrimeFaces.widget.SelectCheckboxMenu.OnShowCallback 
 * 
 * @prop {JQuery} checkboxes The DOM element for the checkboxes that can be selected.
 * @prop {JQuery} defaultLabel The DOM element for the default label.
 * @prop {boolean} disabled Whether this widget is currently disabled.
 * @prop {JQuery} groupHeaders The DOM elements for the headers of each option group.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.FilterFunction} filterMatcher The filter that was selected and is
 * currently used.
 * @prop {Record<PrimeFaces.widget.SelectCheckboxMenu.FilterMatchMode, PrimeFaces.widget.SelectCheckboxMenu.FilterFunction>} filterMatchers
 * Map between the available filter types and the filter implementation.
 * @prop {JQuery} inputs The DOM elements for the hidden inputs for each checkbox option.
 * @prop {boolean} isDynamicLoaded When loading the panel with the available options lazily: if they have been loaded
 * already.
 * @prop {JQuery} items The DOM elements for the the available checkbox options.
 * @prop {JQuery} itemContainer The DOM element for the container with the available checkbox options.
 * @prop {JQuery} itemContainerWrapper The DOM element for the wrapper with the container with the available checkbox
 * options.
 * @prop {JQuery} keyboardTarget The DOM element for the hidden input element that that can be selected via pressing
 * tab. 
 * @prop {JQuery} label The DOM element for the label indicating the currently selected option.
 * @prop {JQuery} labelContainer The DOM element for the container with the label indicating the currently selected
 * option.
 * @prop {string} labelId ID of the label element that indicates the currently selected option.
 * @prop {JQuery} menuIcon The DOM element for the icon for bringing up the overlay panel.
 * @prop {JQuery} multiItemContainer The DOM element for the container with the tags representing the selected options.
 * @prop {JQuery} panel The DOM element for the overlay panel with the available checkbox options.
 * @prop {JQuery} panelId ID of the DOM element for the overlay panel with the available checkbox options.
 * @prop {string} tabindex Tab index of this widget.
 * @prop {JQuery} triggers The DOM elements for the buttons that can trigger (hide or show) the overlay panel with the
 * available checkbox options.
 * @prop {boolean} widthAligned Whether the width of the overlay panel was aligned already.
 * 
 * @interface {PrimeFaces.widget.SelectCheckboxMenuCfg} cfg The configuration for the {@link  SelectCheckboxMenu| SelectCheckboxMenu 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 {string} cfg.appendTo The search expression for the element to which the overlay panel should be appended.
 * @prop {boolean} cfg.caseSensitive Defines if filtering would be case sensitive.
 * @prop {boolean} cfg.dynamic Defines if dynamic loading is enabled for the element's panel. If the value is `true`,
 * the overlay is not rendered on page load to improve performance.
 * @prop {string} cfg.emptyLabel Label to be shown in updateLabel mode when no item is selected. If not set the label is
 * shown.
 * @prop {boolean} cfg.filter `true` if the options can be filtered, or `false` otherwise.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.FilterFunction} cfg.filterFunction A custom filter function that is used
 * when `filterMatchMode` is set to `custom`.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.FilterMatchMode} cfg.filterMatchMode Mode of the filter. When set to
 * `custom`, a `filterFunction` must be specified.
 * @prop {string} cfg.filterPlaceholder Placeholder text to show when filter input is empty.
 * @prop {number} cfg.initialHeight Initial height of the item container.
 * @prop {string} cfg.labelSeparator Separator for joining item lables if updateLabel is set to true. Default is `,`.
 * @prop {boolean} cfg.multiple Whether to show selected items as multiple labels.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.OnChangeCallback} cfg.onChange Callback that is invoked when a checkbox
 * option was checked or unchecked.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.OnHideCallback} cfg.onHide Callback that is invoked when the overlay
 * panel is brought up.
 * @prop {PrimeFaces.widget.SelectCheckboxMenu.OnShowCallback} cfg.onShow Callback that is invoked when the overlay
 * panel is hidden.
 * @prop {string} cfg.panelStyle Inline style of the overlay panel.
 * @prop {string} cfg.panelStyleClass Style class of the overlay panel
 * @prop {number} cfg.scrollHeight Height of the overlay panel.
 * @prop {boolean} cfg.showHeader When enabled, the header of overlay panel is displayed.
 * @prop {boolean} cfg.updateLabel When enabled, the selected items are displayed on the label.
 */
PrimeFaces.widget.SelectCheckboxMenu = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.labelContainer = this.jq.find('.ui-selectcheckboxmenu-label-container');
        this.label = this.jq.find('.ui-selectcheckboxmenu-label');
        this.menuIcon = this.jq.children('.ui-selectcheckboxmenu-trigger');
        this.triggers = this.jq.find('.ui-selectcheckboxmenu-trigger, .ui-selectcheckboxmenu-label');
        this.disabled = this.jq.hasClass('ui-state-disabled');
        this.inputs = this.jq.find(':checkbox');
        this.panelId = this.id + '_panel';
        this.labelId = this.id + '_label';
        this.keyboardTarget = $(this.jqId + '_focus');
        this.tabindex = this.keyboardTarget.attr('tabindex');
        this.cfg.showHeader = (this.cfg.showHeader === undefined) ? true : this.cfg.showHeader;
        this.cfg.dynamic = this.cfg.dynamic === true ? true : false;
        this.isDynamicLoaded = false;
        this.cfg.labelSeparator = (this.cfg.labelSeparator === undefined) ? ', ' : this.cfg.labelSeparator;

        if(!this.disabled) {
            if(this.cfg.multiple) {
                this.triggers = this.jq.find('.ui-selectcheckboxmenu-trigger, .ui-selectcheckboxmenu-multiple-container');
            }

            if(!this.cfg.dynamic) {
                this._renderPanel();
            }

            this.bindEvents();
            this.bindKeyEvents();

            //mark trigger and descandants of trigger as a trigger for a primefaces overlay
            this.triggers.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

            if(!this.cfg.multiple) {
                if(this.cfg.updateLabel) {
                    this.defaultLabel = this.label.text();
                    this.label.css({
                        'text-overflow': 'ellipsis',
                        overflow: 'hidden'
                    });

                    this.updateLabel();
                }

                this.label.attr('id', this.labelId);
                this.keyboardTarget.attr('aria-expanded', false).attr('aria-labelledby', this.labelId);
            }

            this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
        } else {
            // disabled
            if(!this.cfg.multiple) {
                if (this.cfg.updateLabel) {
                    this.defaultLabel = this.label.text();
                    this.label.css({
                        'text-overflow': 'ellipsis',
                        overflow: 'hidden'
                    });

                    this.updateLabel();
                }
            }
        }

        //pfs metadata
        this.inputs.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);
    },

    /**
     * Creates the overlay panel with the checkboxes for the selectable option, and also sets up the event listeners
     * for the panel.
     * @private
     */
    _renderPanel: function() {
        this.renderPanel();

        if(this.tabindex) {
            this.panel.find('a, input').attr('tabindex', this.tabindex);
        }

        this.checkboxes = this.itemContainer.find('.ui-chkbox-box:not(.ui-state-disabled)');
        this.labels = this.itemContainer.find('label');

        this.bindPanelContentEvents();
        this.bindPanelKeyEvents();

        this.isDynamicLoaded = true;
    },

    /**
     * Creates the overlay panel with the checkboxes for the selectable option.
     * @private
     */
    renderPanel: function() {
        this.panel = $('<div id="' + this.panelId + '" class="ui-selectcheckboxmenu-panel ui-widget ui-widget-content ui-corner-all ui-helper-hidden ui-input-overlay" role="dialog"></div>');
        if(this.cfg.panelStyle) {
            this.panel.attr('style', this.cfg.panelStyle);
        }
        if(this.cfg.panelStyleClass) {
            this.panel.addClass(this.cfg.panelStyleClass);
        }
        this.cfg.appendTo = PrimeFaces.utils.resolveAppendTo(this, this.panel);

        PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');

        this.renderHeader();

        this.renderItems();

        if(this.cfg.scrollHeight) {
            this.itemContainerWrapper.height(this.cfg.scrollHeight);
        }
        else if(this.inputs.length > 10) {
            this.itemContainerWrapper.height(200);
        }
    },

    /**
     * Creates the header of the overlay panel with the selectable checkbox options. The header contains the `select all`
     * checkbox, the filter input field and the close icon.
     * @private
     */
    renderHeader: function() {
        this.header = $('<div class="ui-widget-header ui-corner-all ui-selectcheckboxmenu-header ui-helper-clearfix"></div>')
                        .appendTo(this.panel);

        if(!this.cfg.showHeader) {
            this.header.removeClass('ui-helper-clearfix').addClass('ui-helper-hidden');
        }
        //toggler
        this.toggler = $('<div class="ui-chkbox ui-widget"><div class="ui-helper-hidden-accessible"><input type="checkbox" role="checkbox" aria-label="Select All" readonly="readonly"></div><div class="ui-chkbox-box ui-widget ui-corner-all ui-state-default"><span class="ui-chkbox-icon ui-icon ui-icon-blank"></span></div></div>')
                            .appendTo(this.header);
        this.togglerBox = this.toggler.children('.ui-chkbox-box');
        if(this.inputs.filter(':not(:checked)').length === 0) {
            this.check(this.togglerBox);
        }

        //filter
        if(this.cfg.filter) {
            this.filterInputWrapper = $('<div class="ui-selectcheckboxmenu-filter-container"></div>').appendTo(this.header);
            this.filterInput = $('<input type="text" aria-multiline="false" aria-readonly="false" aria-disabled="false" aria-label="Filter Input" role="textbox" class="ui-inputfield ui-inputtext ui-widget ui-state-default ui-corner-all">')
                                .appendTo(this.filterInputWrapper);

            if(this.cfg.filterPlaceholder) {
                this.filterInput.attr('placeholder', this.cfg.filterPlaceholder);
            }

            this.filterInputWrapper.append("<span class='ui-icon ui-icon-search'></span>");
        }

        //closer
        this.closer = $('<a class="ui-selectcheckboxmenu-close ui-corner-all" href="#"><span class="ui-icon ui-icon-circle-close"></span></a>')
                    .attr('aria-label', 'Close').appendTo(this.header);

    },

    /**
     * Creates the individual checkboxes for each selectable option in the overlay panel.
     * @private
     */
    renderItems: function() {
        var $this = this;

        this.itemContainerWrapper = $('<div class="ui-selectcheckboxmenu-items-wrapper"><ul class="ui-selectcheckboxmenu-items ui-selectcheckboxmenu-list ui-widget-content ui-widget ui-corner-all ui-helper-reset"></ul></div>')
                .appendTo(this.panel);

        this.itemContainer = this.itemContainerWrapper.children('ul.ui-selectcheckboxmenu-items');

        //check if inputs must be grouped
        var grouped = this.inputs.filter('[group-label]');

        var currentGroupName = null;
        for(var i = 0; i < this.inputs.length; i++) {
            var input = this.inputs.eq(i),
            label = input.next(),
            disabled = input.is(':disabled'),
            checked = input.is(':checked'),
            title = input.attr('title'),
            boxClass = 'ui-chkbox-box ui-widget ui-corner-all ui-state-default',
            itemClass = 'ui-selectcheckboxmenu-item ui-selectcheckboxmenu-list-item ui-corner-all',
            escaped = input.data('escaped');

            if(grouped.length && currentGroupName !== input.attr('group-label')) {
                currentGroupName = input.attr('group-label');
            	var itemGroup = $('<li class="ui-selectcheckboxmenu-item-group ui-selectcheckboxmenu-group-list-item ui-corner-all"></li>');
            	itemGroup.text(currentGroupName);
            	$this.itemContainer.append(itemGroup);
            }

            if(disabled) {
                boxClass += " ui-state-disabled";
            }

            if(checked) {
                boxClass += " ui-state-active";
            }

            var iconClass = checked ? 'ui-chkbox-icon ui-icon ui-icon-check' : 'ui-chkbox-icon ui-icon ui-icon-blank',
            itemClass = checked ? itemClass + ' ui-selectcheckboxmenu-checked' : itemClass + ' ui-selectcheckboxmenu-unchecked';

            var item = $('<li class="' + itemClass + '"></li>');
            item.append('<div class="ui-chkbox ui-widget"><div class="ui-helper-hidden-accessible"><input type="checkbox" role="checkbox" readonly="readonly"></input></div>' +
                    '<div class="' + boxClass + '"><span class="' + iconClass + '"></span></div></div>');

            var uuid = PrimeFaces.uuid();
            var itemLabel = $('<label for='+uuid+'></label>'),
            labelHtml = label.html().trim(),
            labelLength = labelHtml.length;
            if (labelLength > 0 && labelHtml !== '&nbsp;')
                if(escaped)
                    itemLabel.text(label.text());
                else
                    itemLabel.html(label.html());
            else
                itemLabel.text(input.val());

            itemLabel.appendTo(item);

            if(title) {
                item.attr('title', title);
            }

            if($this.cfg.multiple) {
                item.attr('data-item-value', input.val());
            }

            item.find('> .ui-chkbox > .ui-helper-hidden-accessible > input').prop('checked', checked).attr('aria-checked', checked).attr('id', uuid);
            $this.itemContainer.attr('role', 'group');

            $this.itemContainer.append(item);
        }

        this.items = this.itemContainer.children('li.ui-selectcheckboxmenu-item');
        this.groupHeaders = this.itemContainer.children('li.ui-selectcheckboxmenu-item-group');
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        //Events to show/hide the panel
        this.triggers.on('mouseenter.selectCheckboxMenu', function() {
            if(!$this.disabled) {
                $this.jq.addClass('ui-state-hover');
                $this.triggers.addClass('ui-state-hover');
            }
        }).on('mouseleave.selectCheckboxMenu', function() {
            if(!$this.disabled) {
                $this.jq.removeClass('ui-state-hover');
                $this.triggers.removeClass('ui-state-hover');
            }
        }).on('mousedown.selectCheckboxMenu', function(e) {
            if(!$this.disabled) {
                if($this.cfg.multiple && $(e.target).is('.ui-selectcheckboxmenu-token-icon')) {
                    return;
                }

                if($this.cfg.dynamic && !$this.isDynamicLoaded) {
                    $this._renderPanel();
                }

                if($this.panel.is(":hidden")) {
                    $this.show();
                }
                else {
                    $this.hide();
                }
            }
        }).on('click.selectCheckboxMenu', function(e) {
            $this.keyboardTarget.trigger('focus');
            e.preventDefault();
        });

        if(this.cfg.multiple) {
            this.bindMultipleModeEvents();
        }

        //Client Behaviors
        if(this.cfg.behaviors) {
            PrimeFaces.attachBehaviors(this.inputs, this.cfg.behaviors);
        }
    },

    /**
     * Sets up the event listeners for the overlay panel with the selectable checkbox options.
     * @private
     */
    bindPanelContentEvents: function() {
        var $this = this;

        //Events for checkboxes
        this.bindCheckboxHover(this.checkboxes);
        this.checkboxes.on('click.selectCheckboxMenu', function() {
            $this.toggleItem($(this));
        });

        //Toggler
        this.bindCheckboxHover(this.togglerBox);
        this.togglerBox.on('click.selectCheckboxMenu', function() {
            var el = $(this);
            if(el.hasClass('ui-state-active')) {
                $this.uncheckAll();
            }
            else {
                $this.checkAll();
            }
        });

        //filter
        if(this.cfg.filter) {
            this.setupFilterMatcher();

            PrimeFaces.skinInput(this.filterInput);

            this.filterInput.on('keyup.selectCheckboxMenu', function() {
                $this.filter($(this).val());
            }).on('keydown.selectCheckboxMenu', function(e) {
                if(e.which === $.ui.keyCode.ESCAPE) {
                    $this.hide();
                }
            });
        }

        //Closer
        this.closer.on('mouseenter.selectCheckboxMenu', function(){
            $(this).addClass('ui-state-hover');
        }).on('mouseleave.selectCheckboxMenu', function() {
            $(this).removeClass('ui-state-hover');
        }).on('click.selectCheckboxMenu', function(e) {
            $this.hide();

            e.preventDefault();
        });

        //Labels
        this.labels.on('click.selectCheckboxMenu', function(e) {
            var checkbox = $(this).prev().children('.ui-chkbox-box');
            $this.toggleItem(checkbox);
            checkbox.removeClass('ui-state-hover');
            PrimeFaces.clearSelection();
            e.preventDefault();
        });
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.panel,
            function() { return $this.triggers; },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    $this.hide();
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.panel, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Sets up the event listeners for all keyboard related events other than the overlay panel, such as pressing space
     * to bring up the overlay panel.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        this.keyboardTarget.on('focus.selectCheckboxMenu', function() {
            $this.jq.addClass('ui-state-focus');
            $this.menuIcon.addClass('ui-state-focus');
        }).on('blur.selectCheckboxMenu', function() {
            $this.jq.removeClass('ui-state-focus');
            $this.menuIcon.removeClass('ui-state-focus');
        }).on('keydown.selectCheckboxMenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if($this.cfg.dynamic && !$this.isDynamicLoaded) {
                $this._renderPanel();
            }

            switch(key) {
                case keyCode.ENTER:
                case keyCode.SPACE:
                    if ($this.panel.is(":hidden"))
                        $this.show();
                    else
                        $this.hide();

                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    if (e.altKey) {
                        if ($this.panel.is(":hidden"))
                            $this.show();
                        else
                            $this.hide();
                    }

                    e.preventDefault();
                break;

                case keyCode.TAB:
                    if($this.panel.is(':visible')) {
                       if(!$this.cfg.showHeader) {
                            $this.itemContainer.children('li:not(.ui-state-disabled):first').find('div.ui-helper-hidden-accessible > input').trigger('focus');
                        }
                        else {
                            $this.toggler.find('> div.ui-helper-hidden-accessible > input').trigger('focus');
                        }
                        e.preventDefault();
                    }

                break;

                case keyCode.ESCAPE:
                    $this.hide();
                break;
            };
        });
    },

    /**
     * Sets up the event listeners for all keyboard related events of the overlay panel, such as pressing space to
     * toggle a checkbox.
     * @private
     */
    bindPanelKeyEvents: function() {
        var $this = this;

        this.closer.on('focus.selectCheckboxMenu', function(e) {
            $this.closer.addClass('ui-state-focus');
        })
        .on('blur.selectCheckboxMenu', function(e) {
            $this.closer.removeClass('ui-state-focus');
        })
        .on('keydown.selectCheckboxMenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.ENTER:
                    $this.hide();

                    e.preventDefault();
                break;

                case keyCode.ESCAPE:
                    $this.hide();
                break;
            };
        });

        var togglerCheckboxInput = this.toggler.find('> div.ui-helper-hidden-accessible > input');
        this.bindCheckboxKeyEvents(togglerCheckboxInput);
        togglerCheckboxInput.on('keyup.selectCheckboxMenu', function(e) {
                    if(e.which === $.ui.keyCode.SPACE) {
                        var input = $(this);

                        if(input.prop('checked'))
                            $this.uncheckAll();
                        else
                            $this.checkAll();

                        e.preventDefault();
                    }
                })
                .on('change.selectCheckboxMenu', function(e) {
                    var input = $(this);

                    if(input.prop('checked')) {
                        $this.checkAll();
                    }
                    else {
                        $this.uncheckAll();
                    }
                });

        var itemKeyInputs = this.itemContainer.find('> li > div.ui-chkbox > div.ui-helper-hidden-accessible > input');
        this.bindCheckboxKeyEvents(itemKeyInputs);
        itemKeyInputs.on('keyup.selectCheckboxMenu', function(e) {
                    if(e.which === $.ui.keyCode.SPACE) {
                        var input = $(this),
                        box = input.parent().next();

                        if(input.prop('checked'))
                            $this.uncheck(box, true);
                        else
                            $this.check(box, true);

                        e.preventDefault();
                    }
                })
                .on('change.selectCheckboxMenu', function(e) {
                    var input = $(this),
                    box = input.parent().next();

                    if(input.prop('checked')) {
                        $this.check(box, true);
                    }
                    else {
                        $this.uncheck(box, true);
                    }
                });
    },

    /**
     * When multi select mode is enabled: Sets up the event listeners for the overlay panel.
     * @private
     */
    bindMultipleModeEvents: function() {
        var $this = this;
        this.multiItemContainer = this.jq.children('.ui-selectcheckboxmenu-multiple-container');

        var closeSelector = '> li.ui-selectcheckboxmenu-token > .ui-selectcheckboxmenu-token-icon';
        this.multiItemContainer.off('click', closeSelector).on('click', closeSelector, null, function(e) {
            var itemValue = $(this).parent().data("item-value");
            var item = $this.items.filter('[data-item-value="' + $.escapeSelector(itemValue) +'"]');
            if(item && item.length) {
                if($this.cfg.dynamic && !$this.isDynamicLoaded) {
                    $this._renderPanel();
                }

                $this.uncheck(item.children('.ui-chkbox').children('.ui-chkbox-box'), true);

                if($this.hasBehavior('itemUnselect')) {
                    var ext = {
                        params : [
                            {name: $this.id + '_itemUnselect', value: itemValue}
                        ]
                    };

                    $this.callBehavior('itemUnselect', ext);
                }
            }

            e.stopPropagation();
        });
    },

    /**
     * Sets up the event listeners for hovering over the checkboxes. Adds the appropriate hover style classes.
     * @private
     * @param {JQuery} item A checkbox for which to add the event listeners.
     */
    bindCheckboxHover: function(item) {
        item.on('mouseenter.selectCheckboxMenu', function() {
            var item = $(this);
            if(!item.hasClass('ui-state-disabled')) {
                item.addClass('ui-state-hover');
            }
        }).on('mouseleave.selectCheckboxMenu', function() {
            $(this).removeClass('ui-state-hover');
        });
    },

    /**
     * Filters the available options in the overlay panel by the given search value. Note that this does not bring up
     * the overlay panel, use `show` for that.
     * @param {string} value A value against which the available options are matched.
     */
    filter: function(value) {
        var filterValue = this.cfg.caseSensitive ? PrimeFaces.trim(value) : PrimeFaces.trim(value).toLowerCase();

        if(filterValue === '') {
            this.itemContainer.children('li.ui-selectcheckboxmenu-item').filter(':hidden').show();
        }
        else {
            for(var i = 0; i < this.labels.length; i++) {
                var labelElement = this.labels.eq(i),
                item = labelElement.parent(),
                itemLabel = this.cfg.caseSensitive ? labelElement.text() : labelElement.text().toLowerCase();

                if(this.filterMatcher(itemLabel, filterValue)) {
                    item.show();
                }
                else {
                    item.hide();
                }
            }
        }

        var groupHeaderLength = this.groupHeaders.length;
        for(var i = 0; i < groupHeaderLength; i++) {
            var header = $(this.groupHeaders[i]),
            groupedItems = header.nextUntil('li.ui-selectcheckboxmenu-item-group');

            if(groupedItems.length === groupedItems.filter(':hidden').length) {
                header.hide();
            }
            else {
                header.show();
            }
        }

        if(this.cfg.scrollHeight) {
            if(this.itemContainer.height() < this.cfg.initialHeight) {
                this.itemContainerWrapper.css('height', 'auto');
            }
            else {
                this.itemContainerWrapper.height(this.cfg.initialHeight);
            }
        }

        this.updateToggler();
        this.alignPanel();
    },

    /**
     * Finds and stores the filter function which is to be used for filtering the options of this select checkbox menu.
     * @private
     */
    setupFilterMatcher: function() {
        this.cfg.filterMatchMode = this.cfg.filterMatchMode||'startsWith';
        this.filterMatchers = {
            'startsWith': this.startsWithFilter
            ,'contains': this.containsFilter
            ,'endsWith': this.endsWithFilter
            ,'custom': this.cfg.filterFunction
        };

        this.filterMatcher = this.filterMatchers[this.cfg.filterMatchMode];
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectCheckboxMenu.FilterFunction` that matches the given option when it
     * starts with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options starts with the filter value, or `false` otherwise.
     */
    startsWithFilter: function(value, filter) {
        return value.indexOf(filter) === 0;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectCheckboxMenu.FilterFunction` that matches the given option when it
     * contains the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the contains the filter value, or `false` otherwise.
     */
    containsFilter: function(value, filter) {
        return value.indexOf(filter) !== -1;
    },

    /**
     * Implementation of a `PrimeFaces.widget.SelectCheckboxMenu.FilterFunction` that matches the given option when it
     * ends with the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options ends with the filter value, or `false` otherwise.
     */
    endsWithFilter: function(value, filter) {
        return value.indexOf(filter, value.length - filter.length) !== -1;
    },

    /**
     * Selects all available options. Note that this required the overlay panel to be visible, use `show` for that.
     */
    checkAll: function() {
        for(var i = 0; i < this.items.length; i++) {
            var el = this.items.eq(i);

            if(el.is(':visible')) {
                var input = this.inputs.eq(i);
                var inputNative = input[0];

                if(!inputNative.disabled) {
                    input.prop('checked', true).attr('aria-checked', true);
                    this.check(el.children('.ui-chkbox').children('.ui-chkbox-box'));

                    if (this.cfg.multiple) {
                        this.createMultipleItem(el);
                    }
                }
            }
        }

        this.check(this.togglerBox);

        var togglerInput = this.togglerBox.prev().children('input');
        if(this.cfg.onChange) {
            this.cfg.onChange.call(this);
        }

        if(!this.togglerBox.hasClass('ui-state-disabled')) {
            togglerInput.trigger('focus.selectCheckboxMenu');
            this.togglerBox.addClass('ui-state-active');
        }

        if(this.cfg.multiple) {
            this.alignPanel();
        }

        this.fireToggleSelectEvent(true);
    },

    /**
     * Unselects all available options. Note that this required the overlay panel to be visible, use `show` for that.
     */
    uncheckAll: function() {
        for(var i = 0; i < this.items.length; i++) {
            var el = this.items.eq(i);

            if(el.is(':visible')) {
                var input = this.inputs.eq(i);
                var inputNative = input[0];

                if(!inputNative.disabled) {
                    this.inputs.eq(i).prop('checked', false).attr('aria-checked', false);
                    this.uncheck(el.children('.ui-chkbox').children('.ui-chkbox-box'));

                    if (this.cfg.multiple) {
                        this.multiItemContainer.children().remove();
                    }
                }
            }
        }

        this.uncheck(this.togglerBox);

        var togglerInput = this.togglerBox.prev().children('input');
        if(this.cfg.onChange) {
            this.cfg.onChange.call(this);
        }

        if(!this.togglerBox.hasClass('ui-state-disabled')) {
            togglerInput.trigger('focus.selectCheckboxMenu');
        }

        if(this.cfg.multiple) {
            this.alignPanel();
        }

        this.fireToggleSelectEvent(false);
    },

    /**
     * Triggers the select behavior, if any, when a checkbox option was selected or unselected.
     * @private
     * @param {boolean} checked Whether the checkbox option is now checked.
     */
    fireToggleSelectEvent: function(checked) {
        if(this.hasBehavior('toggleSelect')) {
            var ext = {
                params: [{name: this.id + '_checked', value: checked}]
            };

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

    /**
     * Selects the given checkbox option.
     * @private
     * @param {JQuery} checkbox Checkbox option to select.
     * @param {boolean} updateInput If `true`, update the hidden input field with the current value of this widget.
     */
    check: function(checkbox, updateInput) {
        if(!checkbox.hasClass('ui-state-disabled')) {
            var checkedInput = checkbox.prev().children('input'),
            item = checkbox.closest('li.ui-selectcheckboxmenu-item');

            checkedInput.prop('checked', true).attr('aria-checked', true);
            if(updateInput) {
                checkedInput.trigger('focus.selectCheckboxMenu');
            }

            checkbox.addClass('ui-state-active').children('.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
            item.removeClass('ui-selectcheckboxmenu-unchecked').addClass('ui-selectcheckboxmenu-checked');

            if(updateInput) {
            	var itemGroups = item.prevAll('li.ui-selectcheckboxmenu-item-group'),
                input = this.inputs.eq(item.index() - itemGroups.length);
                input.prop('checked', true).attr('aria-checked', true).trigger('change');

                this.updateToggler();

                if(this.cfg.multiple) {
                    this.createMultipleItem(item);
                    this.alignPanel();
                }
            }

            if(this.cfg.updateLabel) {
                this.updateLabel();
            }
        }
    },

    /**
     * Unselects the given checkbox option.
     * @private
     * @param {JQuery} checkbox Checkbox option to unselect.
     * @param {boolean} updateInput If `true`, update the hidden input field with the current value of this widget.
     */
    uncheck: function(checkbox, updateInput) {
        if(!checkbox.hasClass('ui-state-disabled')) {
            var uncheckedInput = checkbox.prev().children('input'),
            item = checkbox.closest('li.ui-selectcheckboxmenu-item');
            checkbox.removeClass('ui-state-active').children('.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
            checkbox.closest('li.ui-selectcheckboxmenu-item').addClass('ui-selectcheckboxmenu-unchecked').removeClass('ui-selectcheckboxmenu-checked');
            uncheckedInput.prop('checked', false).attr('aria-checked', false);

            if(updateInput) {
                var itemGroups = item.prevAll('li.ui-selectcheckboxmenu-item-group'),
                input = this.inputs.eq(item.index() - itemGroups.length);
                input.prop('checked', false).attr('aria-checked', false).trigger('change');
                uncheckedInput.trigger('focus.selectCheckboxMenu');
                this.updateToggler();

                if(this.cfg.multiple) {
                    this.removeMultipleItem(item);
                    this.alignPanel();
                }
            }

            if(this.cfg.updateLabel) {
                this.updateLabel();
            }
        }
    },

    /**
     * Brings up the overlay panel with the available checkbox options.
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.panel.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.keyboardTarget.attr('aria-expanded', true);
                    $this.postShow();
                    $this.bindPanelEvents();
                }
            });
        }
    },

    /**
     * Hides the overlay panel with the available checkbox options.
     */
    hide: function() {
        if (this.panel.is(':visible') && this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.keyboardTarget.attr('aria-expanded', false);
                    $this.postHide();
                }
            });
        }
    },

    /**
     * Callback that is invoked after the overlay panel with the checkbox options was made visible.
     * @private
     */
    postShow: function() {
        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }
    },

    /**
     * Callback that is invoked after the overlay panel with the checkbox options was hidden.
     * @private
     */
    postHide: function() {
        if(this.cfg.onHide) {
            this.cfg.onHide.call(this);
        }
    },

    /**
     * Align the overlay panel with the available checkbox options so that is is positioned next to the the button.
     */
    alignPanel: function() {
        var fixedPosition = this.panel.css('position') == 'fixed',
        win = $(window),
        positionOffset = fixedPosition ? '-' + win.scrollLeft() + ' -' + win.scrollTop() : null,
        panelStyle = this.panel.attr('style');

        this.panel.css({
                'left':'',
                'top':'',
                'z-index': PrimeFaces.nextZindex(),
                'transform-origin': 'center top'
        });

        if(this.panel.parent().attr('id') === this.id) {
            this.panel.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px'
            });
        }
        else {
            this.panel.position({
                                my: 'left top'
                                ,at: 'left bottom'
                                ,of: this.jq
                                ,offset : positionOffset
                                ,collision: 'flipfit'
                                ,using: function(pos, directions) {
                                    $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                                }
                            });
        }

        if(!this.widthAligned && (this.panel.width() < this.jq.width()) && (!panelStyle||panelStyle.toLowerCase().indexOf('width') === -1)) {
            this.panel.width(this.jq.width());
            this.widthAligned = true;
        }
    },

    /**
     * Select or unselect the given checkbox option.
     * @param {JQuery} checkbox One of the checkbox options of this widget to toggle.
     */
    toggleItem: function(checkbox) {
        if(!checkbox.hasClass('ui-state-disabled')) {
            if(checkbox.hasClass('ui-state-active')) {
                this.uncheck(checkbox, true);
            }
            else {
                this.check(checkbox, true);
            }
        }
    },

    /**
     * Updates the `select all` / `unselect all` toggler so that it reflects the currently selected options.
     * @private
     */
    updateToggler: function() {
        var visibleItems = this.itemContainer.children('li.ui-selectcheckboxmenu-item:visible');

        if(visibleItems.length && visibleItems.filter('.ui-selectcheckboxmenu-unchecked').length === 0) {
            this.check(this.togglerBox);
        }
        else {
            this.uncheck(this.togglerBox);
        }
    },

    /**
     * Sets up the keyboard event listeners for the given checkbox options.
     * @private
     * @param {JQuery} items Checkbo options for which to add the event listeners.
     */
    bindCheckboxKeyEvents: function(items) {
        var $this = this;

        items.on('focus.selectCheckboxMenu', function(e) {
            var input = $(this),
            box = input.parent().next();

            box.addClass('ui-state-focus');

            PrimeFaces.scrollInView($this.itemContainerWrapper, box);
        })
        .on('blur.selectCheckboxMenu', function(e) {
            var input = $(this),
            box = input.parent().next();

            box.removeClass('ui-state-focus');
        })
        .on('keydown.selectCheckboxMenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE) {
                e.preventDefault();
            }
            else if(key === keyCode.ESCAPE) {
                $this.hide();
            }
        });
    },

    /**
     * When multi mode is disabled: Upates the label that indicates the currently selected item.
     * @private
     */
    updateLabel: function() {
        var checkedItems = this.jq.find(':checked'),
            labelText = '';

        if(checkedItems && checkedItems.length) {
            for(var i = 0; i < checkedItems.length; i++) {
                if(i != 0) {
                    labelText = labelText + this.cfg.labelSeparator;
                }
                labelText = labelText + $(checkedItems[i]).next().text();
            }
        }
        else {
            if (this.cfg.emptyLabel) {
                labelText = this.cfg.emptyLabel;
            } else {
                labelText = this.defaultLabel;
            }
        }

        this.label.text(labelText);
        this.labelContainer.attr('title', labelText);
    },

    /**
     * When multi mode is enabled: Creates a tag for the given item that was checked.
     * @private
     * @param {JQuery} item The checkbox item that was checked. 
     */
    createMultipleItem: function(item) {
        var items = this.multiItemContainer.children();
        if(items.length && items.filter('[data-item-value="' + $.escapeSelector(item.data('item-value')) + '"]').length > 0) {
            return;
        }

        var itemGroups = item.prevAll('li.ui-selectcheckboxmenu-item-group'),
        input = this.inputs.eq(item.index() - itemGroups.length),
        escaped = input.data('escaped'),
        labelHtml = input.next().html().trim(),
        labelLength = labelHtml.length,
        label = labelLength > 0 && labelHtml !== '&nbsp;' ? (escaped ? PrimeFaces.escapeHTML(input.next().text()) : input.next().html()) : PrimeFaces.escapeHTML(input.val()),
        itemDisplayMarkup = '<li class="ui-selectcheckboxmenu-token ui-state-active ui-corner-all" data-item-value="' + PrimeFaces.escapeHTML(input.val()) +'">';
        itemDisplayMarkup += '<span class="ui-selectcheckboxmenu-token-icon ui-icon ui-icon-close"></span>';
        itemDisplayMarkup += '<span class="ui-selectcheckboxmenu-token-label">' + label + '</span></li>';

        this.multiItemContainer.append(itemDisplayMarkup);
    },

    /**
     * When multi mode is enabled: Removes all visible tags with the same value as the given checkbox item.
     * @private
     * @param {JQuery} item Checkbox item that was unchecked.
     */
    removeMultipleItem: function(item) {
        var items = this.multiItemContainer.children();
        if(items.length) {
            items.filter('[data-item-value="' + $.escapeSelector(item.data('item-value')) + '"]').remove();
        }
    },

    /**
     * Checks the checkbox option with the given value.
     * @param {string} value Value of the option to check.
     */
    selectValue: function(value) {                                                                     // Patch
        var idx = -1;
        // find input-index
        for(var i = 0; i < this.inputs.length; i++) {
            if (this.inputs.eq(i).val() === value) {
                idx = i;
                break;
            }
        }
        if (idx === -1) {
            return;
        }
        var input = this.inputs.eq(idx);   // the hidden input
        var item  = this.items.eq(idx);    // the Overlay-Panel-Item (li)

        // check (see this.checkAll())
        input.prop('checked', true).attr('aria-checked', true);
        this.check(item.children('.ui-chkbox').children('.ui-chkbox-box'));

        if(this.cfg.multiple) {
            this.createMultipleItem(item);
        }
    }

});

/**
 * __PrimeFaces Password Widget__
 * 
 * Password component is an extended version of standard inputSecret component with theme integration and strength
 * indicator.
 * 
 * @prop {JQuery} panel The DOM element for the overlay panel with the hint regarding how strong the password is.
 * @prop {JQuery} meter The DOM element for the gauge giving visual feedback regarding how strong the password is.
 * @prop {JQuery} infoText The DOM element for the informational text regarding how strong the password is.
 * @prop {JQuery} icon The DOM element for mask/unmask icon
 * 
 * @interface {PrimeFaces.widget.PasswordCfg} cfg The configuration for the {@link  Password| Password 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.feedback Enables strength indicator.
 * @prop {string} cfg.promptLabel Label of the password prompt.
 * @prop {string} cfg.weakLabel Text of the hint when the password is judged to be weak.
 * @prop {string} cfg.goodLabel Text of the hint when the password is judged to be good.
 * @prop {string} cfg.strongLabel Text of the hint when the password is judged to be strong.
 * @prop {boolean} cfg.inline Displays feedback inline rather than using a popup.
 * @prop {string} cfg.showEvent Event displaying the feedback overlay. Default is 'focus'.
 * @prop {string} cfg.hideEvent Event hiding the feedback overlay. Default is 'blur'.
 * @prop {boolean} cfg.unmaskable Whether or not this password can be unmasked/remasked.
 */
PrimeFaces.widget.Password = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        if (!this.jq.is(':disabled')) {
            if (this.cfg.feedback) {
                this.setupFeedback();
            }

            if (this.cfg.unmaskable) {
                this.setupUnmasking();
            }

            PrimeFaces.skinInput(this.jq);
        }
    },

    /**
     * Sets up the overlay panel informing the user about how good the password their typed is.
     * @private
     */
    setupFeedback: function() {
        var $this = this;

        //remove previous panel if any
        var oldPanel = $(this.jqId + '_panel');
        if(oldPanel.length == 1) {
            oldPanel.remove();
        }

        //config
        this.cfg.showEvent = this.cfg.showEvent ? this.cfg.showEvent + '.password' : 'focus.password';
        this.cfg.hideEvent = this.cfg.hideEvent ? this.cfg.hideEvent + '.password' : 'blur.password';
        this.cfg.promptLabel = this.cfg.promptLabel||'Please enter a password';
        this.cfg.weakLabel = this.cfg.weakLabel||'Weak';
        this.cfg.goodLabel = this.cfg.goodLabel||'Medium';
        this.cfg.strongLabel = this.cfg.strongLabel||'Strong';

        var panelStyle = this.cfg.inline ? 'ui-password-panel-inline' : 'ui-password-panel-overlay';

        //create panel element
        var panelMarkup = '<div id="' + this.id + '_panel" class="ui-password-panel ui-widget ui-state-highlight ui-corner-all ui-helper-hidden ' + panelStyle + '">';
        panelMarkup += '<div class="ui-password-meter" style="background-position:0pt 0pt">&nbsp;</div>';
        panelMarkup += '<div class="ui-password-info">' + PrimeFaces.escapeHTML(this.cfg.promptLabel) + '</div>';
        panelMarkup += '</div>';

        this.panel = $(panelMarkup).insertAfter(this.jq);
        this.meter = this.panel.children('div.ui-password-meter');
        this.infoText = this.panel.children('div.ui-password-info');

        if (!this.cfg.inline) {
            this.panel.addClass('ui-shadow');
        }

        //events
        this.jq.off(this.cfg.showEvent + ' ' + this.cfg.hideEvent + ' keyup.password')
        .on(this.cfg.showEvent, function() {
            $this.show();
        })
        .on(this.cfg.hideEvent, function() {
            $this.hide();
        })
        .on("keyup.password", function() {
            var value = $this.jq.val(),
            label = null,
            meterPos = null;

            if(value.length == 0) {
                label = $this.cfg.promptLabel;
                meterPos = '0px 0px';
            }
            else {
                var score = $this.testStrength($this.jq.val());

                if(score < 30) {
                    label = $this.cfg.weakLabel;
                    meterPos = '0px -10px';
                }
                else if(score >= 30 && score < 80) {
                    label = $this.cfg.goodLabel;
                    meterPos = '0px -20px';
                }
                else if(score >= 80) {
                    label = $this.cfg.strongLabel;
                    meterPos = '0px -30px';
                }
            }

            //update meter and info text
            $this.meter.css('background-position', meterPos);
            $this.infoText.text(label);
        });

        //overlay setting
        if (!this.cfg.inline) {
            this.panel.appendTo('body');
            this.transition = PrimeFaces.utils.registerCSSTransition(this.panel, 'ui-connected-overlay');
        }
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        //Hide overlay on resize/scroll
        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.panel, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Sets up the ability to unmask and remask the password.
     * @private
     */
    setupUnmasking: function() {
        var $this = this;
        this.icon = $(PrimeFaces.escapeClientId(this.id + '_mask'));
        this.icon.off('click.password').on('click.password', function() {
            $this.toggleMask();
        });
    },
    
    /**
     * Toggle masking and unmasking the password.
     */
    toggleMask: function() {
        if(!this.cfg.unmaskable) {
            return;
        }

        if (this.jq.attr('type') === 'password') {
            this.jq.attr('type', 'text').parent().removeClass('ui-password-masked').addClass('ui-password-unmasked');
        } 
        else {
            this.jq.attr('type', 'password').parent().removeClass('ui-password-unmasked').addClass('ui-password-masked');
        } 
    },

    /**
     * Computes a numerical score that estimates how strong the given password is. The returned value can range from `0`
     * (very weak) to `128` (very strong). This test takes into account whether the password has got a certain minimal
     * length and whether it contains characters from certain character classes.
     * @param {string} password A password to check for its strength.
     * @return {number} A value between `0` and `128` that indicates how good the password is, with `0` indicating a
     * very weak password and `128` indicating a very strong password.
     */
    testStrength: function(password) {
        // return a number between 0 and 100.
        var score = 0;

        // must be at least 8 characters
        if (!password || password.length < 8)
            return score;

        // require 3 of the following 4 categories
        var variations = {
            digits : /\d/.test(password),
            lower : /[a-z]/.test(password),
            upper : /[A-Z]/.test(password),
            nonWords : /\W/.test(password)
        }

        variationCount = 0;
        for ( var check in variations) {
            variationCount += (variations[check] == true) ? 1 : 0;
        }
        score += variationCount * 28;

        return parseInt(score);
    },

    /**
     * Returns a normalized value between `0` and `1.5` that indicates how much bigger the first input x is compared
     * to the other input y. `0` means that x is much smaller than `y`, a value of `1.5` mean that `x` is much larger
     * than `y`.
     * @private
     * @param {number} x First input, must be a non-negative number.
     * @param {number} y  Second input, must be a positive number
     * @return {number} A value between `0` and `1.5` that indicates how big `x` is compared to `y`.
     */
    normalize: function(x, y) {
        var diff = x - y;

        if (diff <= 0) {
            return x / y;
        }
        else {
            return 1 + 0.5 * (x / (x + y/4));
        }
    },

    /**
     * Align the panel with the password strength indicator so that it is next to the password input field.
     * @private
     */
    align: function() {
        this.panel.css({
            left:'',
            top:'',
            'min-width': this.jq.outerWidth(),
            'transform-origin': 'center top'
        })
        .position({
            my: 'left top'
            ,at: 'left bottom'
            ,of: this.jq
            ,collision: 'flipfit'
            ,using: function(pos, directions) {
                $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
            }
        });
    },

    /**
     * Brings up the panel with the password strength indicator.
     */
    show: function() {
        if (!this.cfg.inline) {
            var $this = this;
    
            if (this.transition) {
                this.transition.show({
                    onEnter: function() {
                        $this.panel.css('z-index', PrimeFaces.nextZindex());
                        $this.align();
                    },
                    onEntered: function() {
                        $this.bindPanelEvents();
                    }
                });
            }
        }
        else {
            this.panel.css({ width: this.jq.outerWidth()});
            this.panel.slideDown();
        }
    },

    /**
     * Hides the panel with the password strength indicator.
     */
    hide: function() {
        if (this.cfg.inline) {
            this.panel.slideUp();
        }
        else if (this.transition) {
            var $this = this;
    
            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                }
            });
        }
    }
    
});

/**
 * __PrimeFaces DefaultCommand Widget__
 * 
 * Which command to submit the form with when enter key is pressed a common problem in web apps not just specific to
 * JSF. Browsers tend to behave differently as there doesn’t seem to be a standard and even if a standard exists,
 * IE probably will not care about it. There are some ugly workarounds like placing a hidden button and writing
 * JavaScript for every form in your app. `DefaultCommand` solves this problem by normalizing the command (e.g. button
 * or link) to submit the form with on enter key press.
 * 
 * @prop {JQuery} jqTarget The DOM element for the target button or link.
 * @prop {JQuery | null} scope The ancestor component to enable multiple default commands in a form. 
 * 
 * @interface {PrimeFaces.widget.DefaultCommandCfg} cfg The configuration for the {@link  DefaultCommand| DefaultCommand 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 {string} cfg.scope Identifier of the ancestor component to enable multiple default commands in a form.
 * @prop {string} cfg.target Identifier of the default command component.
 */
PrimeFaces.widget.DefaultCommand = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.jqTarget = $(PrimeFaces.escapeClientId(this.cfg.target));
        this.scope = this.cfg.scope ? $(PrimeFaces.escapeClientId(this.cfg.scope)) : null;
        var $this = this;

        // container support - e.g. splitButton
        if (this.jqTarget.is(':not(:button):not(:input):not(a)')) {
            this.jqTarget = this.jqTarget.find('button,a').filter(':visible').first();
        }

        //attach keypress listener to parent form
        var closestForm = this.jqTarget.closest('form');
        closestForm.off('keydown.' + this.id).on('keydown.' + this.id, {scopeEnter: false}, function (e, data) {
            var keyCode = $.ui.keyCode;

            data = data || e.data;
            if (($this.scope && data.scopeEnter && data.scopeDefaultCommandId === $this.id)
                    || (!$this.scope && !data.scopeEnter && (e.which == keyCode.ENTER))) {
                //do not proceed if target is a textarea,button or link
                if ($(e.target).is('textarea,button,input[type="submit"],a')) {
                    return true;
                }

                if (!$this.jqTarget.is(':disabled, .ui-state-disabled')) {
                    $this.jqTarget.trigger(PrimeFaces.csp.clickEvent());
                }
                e.preventDefault();
                e.stopImmediatePropagation();
            }
        });

        if (this.scope) {
            this.scope.off('keydown.' + this.id).on('keydown.' + this.id, function (e) {
                var keyCode = $.ui.keyCode;
                if (e.which == keyCode.ENTER) {
                    closestForm.trigger(e, {scopeEnter: true, scopeDefaultCommandId: $this.id});
                    //e.preventDefault();
                    e.stopPropagation();
                }
            });
        }
    }
});

/**
 * __PrimeFaces SplitButton Widget__
 * 
 * SplitButton displays a command by default and additional ones in an overlay.
 * 
 * @typedef PrimeFaces.widget.SplitButton.FilterFunction A filter function that takes a term and returns whether the
 * search term matches the value.
 * @param {string} PrimeFaces.widget.SplitButton.FilterFunction.value A value to check.
 * @param {string} PrimeFaces.widget.SplitButton.FilterFunction.query A search term against which the value is checked.
 * @return {string} PrimeFaces.widget.SplitButton.FilterFunction `true` if the search term matches the value, or `false`
 * otherwise.
 * 
 * @typedef {"startsWith" |  "contains" |  "endsWith" | "custom"} PrimeFaces.widget.SplitButton.FilterMatchMode
 * Available modes for filtering the options of the available buttons actions of a split button. When `custom` is set, a
 * `filterFunction` must be specified.

 * @prop {JQuery} button The DOM element for the main button.
 * @prop {PrimeFaces.widget.SplitButton.FilterFunction} filterMatcher The current filter function.
 * @prop {Record<string, PrimeFaces.widget.SplitButton.FilterFunction>} filterMatchers A map of all flter functions. The
 * key is the name of the filter function.
 * @prop {JQuery} filterInput The DOM element for the filter input field
 * @prop {JQuery} menu The DOM element for the additional buttons actions. 
 * @prop {JQuery} menuitemContainer The DOM element for the container of the additional buttons actions.
 * @prop {JQuery} menuitems The DOM elements for the individual additional button actions.
 * @prop {JQuery} menuButton The DOM element for the button that triggers the overlay panel with the additional buttons
 * actions.
 * @prop {string} menuId The prefix shared ny the different IDs of the components of this widget.
 * 
 * @interface {PrimeFaces.widget.SplitButtonCfg} cfg The configuration for the {@link  SplitButton| SplitButton 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 {string} cfg.appendTo The search expression for the element to which the overlay panel should be appended.
 * @prop {PrimeFaces.widget.SplitButton.FilterMatchMode} cfg.filterMatchMode Match mode for filtering, how the search
 * term is matched against the items.
 * @prop {boolean} cfg.disabled Whether this input is currently disabled.
 * @prop {boolean} cfg.filter Whether client side filtering feature is enabled.
 * @prop {PrimeFaces.widget.SplitButton.FilterFunction} cfg.filterFunction Custom JavaScript function for filtering the
 * available split button actions.
 */
PrimeFaces.widget.SplitButton = PrimeFaces.widget.BaseWidget.extend({

     /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.button = $(this.jqId + '_button');
        this.menuButton = $(this.jqId + '_menuButton');
        this.menuId = this.jqId + "_menu";
        this.menu = $(this.menuId);
        this.menuitemContainer = this.menu.find('.ui-menu-list');
        this.menuitems = this.menuitemContainer.children('.ui-menuitem:not(.ui-state-disabled)');
        this.cfg.disabled = this.button.is(':disabled');

        if(!this.cfg.disabled) {
            this.bindEvents();

            PrimeFaces.utils.registerDynamicOverlay(this, this.menu, this.id + '_menu');
            this.transition = PrimeFaces.utils.registerCSSTransition(this.menu, 'ui-connected-overlay');
        }

        //pfs metadata
        this.button.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        this.menuButton.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);
    },

    /**
     * Sets up all event listenters required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        PrimeFaces.skinButton(this.button).skinButton(this.menuButton);

        //mark button and descandants of button as a trigger for a primefaces overlay
        this.button.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

        //toggle menu
        this.menuButton.on("click", function() {
            if($this.menu.is(':hidden'))
                $this.show();
            else
                $this.hide();
        });

        //menuitem visuals
        this.menuitems.on("mouseover", function(e) {
            var menuitem = $(this),
            menuitemLink = menuitem.children('.ui-menuitem-link');

            if(!menuitemLink.hasClass('ui-state-disabled')) {
                menuitem.addClass('ui-state-hover');
            }
        }).on("mouseout", function(e) {
            $(this).removeClass('ui-state-hover');
        }).on("click", function() {
            $this.hide();
        });

        //keyboard support
        this.menuButton.on("keydown", function(e) {
            var keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.UP:
                    $this.highlightPrev(e);
                break;

                case keyCode.DOWN:
                    $this.highlightNext(e);
                break;

                case keyCode.ENTER:
                case keyCode.SPACE:
                    $this.handleEnterKey(e);
                break;


                case keyCode.ESCAPE:
                case keyCode.TAB:
                    $this.handleEscapeKey();
                break;
            }
        });

        if(this.cfg.filter) {
            this.setupFilterMatcher();
            this.filterInput = this.menu.find('> div.ui-splitbuttonmenu-filter-container > input.ui-splitbuttonmenu-filter');
            PrimeFaces.skinInput(this.filterInput);

            this.bindFilterEvents();
        }
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.menu, null,
            function(e, eventTarget) {
                if(!($this.menu.is(eventTarget) || $this.menu.has(eventTarget).length > 0)) {
                    $this.button.removeClass('ui-state-focus ui-state-hover');
                    $this.hide();
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', this.menu, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },
    
    /**
     * Sets up the event listeners for filtering the available buttons actions via a search field.
     * @private
     */
    bindFilterEvents: function() {
        var $this = this;

        this.filterInput.on('keyup.ui-splitbutton', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                case keyCode.LEFT:
                case keyCode.DOWN:
                case keyCode.RIGHT:
                case keyCode.ENTER:
                case keyCode.TAB:
                case keyCode.ESCAPE:
                case keyCode.SPACE:
                case keyCode.HOME:
                case keyCode.PAGE_DOWN:
                case keyCode.PAGE_UP:
                case keyCode.END:
                case 16: //shift
                case 17: //keyCode.CONTROL:
                case 18: //keyCode.ALT:
                case 91: //left window or cmd:
                case 92: //right window:
                case 93: //right cmd:
                case 20: //capslock:
                break;

                default:
                    //function keys (F1,F2 etc.)
                    if(key >= 112 && key <= 123) {
                        break;
                    }

                    var metaKey = e.metaKey||e.ctrlKey;

                    if(!metaKey) {
                        $this.filter($(this).val());
                    }
                break;
            }
        })
        .on('keydown.ui-splitbutton',function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            switch(key) {
                case keyCode.UP:
                    $this.highlightPrev(e);
                break;

                case keyCode.DOWN:
                    $this.highlightNext(e);
                break;
                
                case keyCode.ENTER:
                    $this.handleEnterKey(e);
                break;
                
                case keyCode.SPACE:
                    var target = $(e.target);

                    if(target.is('input') && target.hasClass('ui-splitbuttonmenu-filter')) {
                        return;
                    }
                    
                    $this.handleEnterKey(e);
                break;
                
                case keyCode.ESCAPE:
                case keyCode.TAB:
                    $this.handleEscapeKey();
                break;

                default:
                break;
            }
        }).on('paste.ui-splitbutton', function() {
            setTimeout(function(){
                $this.filter($this.filterInput.val());
            },2);
	});
    },
    
    /**
     * Highlights the next button action, usually when the user navigates via the keyboard arrows.
     * @private
     * @param {JQuery.TriggeredEvent} event Keyboard arrow event that caused the next item to be highlighted.
     */
    highlightNext: function(event) {
        var highlightedItem = this.menuitems.filter('.ui-state-hover'),
        nextItems = highlightedItem.length ? highlightedItem.nextAll(':not(.ui-separator, .ui-widget-header):visible') : this.menuitems.filter(':visible').eq(0);
        
        if(nextItems.length) {
            highlightedItem.removeClass('ui-state-hover');
            nextItems.eq(0).addClass('ui-state-hover');
        }

        event.preventDefault();
    },
    
    /**
     * Highlights the previous button action, usually when the user navigates via the keyboard arrows.
     * @private
     * @param {JQuery.TriggeredEvent} event Keyboard arrow event that caused the previous item to be highlighted.
     */
    highlightPrev: function(event) {
        var highlightedItem = this.menuitems.filter('.ui-state-hover'),
        prevItems = highlightedItem.length ? highlightedItem.prevAll(':not(.ui-separator, .ui-widget-header):visible') : null;

        if(prevItems && prevItems.length) {
            highlightedItem.removeClass('ui-state-hover');
            prevItems.eq(0).addClass('ui-state-hover');
        }

        event.preventDefault();
    },
    
    /**
     * Callback that is invoked when the enter key is pressed. When overlay panel with the additional buttons actions is
     * shown, activates the selected buttons action. Otherwise, opens the overlay panel. 
     * @private
     * @param {JQuery.TriggeredEvent} event Keyboard event of the enter press.
     */
    handleEnterKey: function(event) {
        if(this.menu.is(':visible')) {
            var link = this.menuitems.filter('.ui-state-hover').children('a');
            link.trigger('click');
            
            var href = link.attr('href');
            if(href && href !== '#') {
                window.location.href = href;
            } 
        }
        else {
            this.show();
        }

        event.preventDefault();
    },
    
    /**
     * Callback that is invoked when the escape key is pressed while the overlay panel with the additional buttons
     * actions is shown. Hides that overlay panel.
     * @private
     */
    handleEscapeKey: function() {
        this.hide();
    },

    /**
     * Creates the filter functions for filtering the button actions.
     * @private
     */
    setupFilterMatcher: function() {
        this.cfg.filterMatchMode = this.cfg.filterMatchMode||'startsWith';
        this.filterMatchers = {
            'startsWith': this.startsWithFilter
            ,'contains': this.containsFilter
            ,'endsWith': this.endsWithFilter
            ,'custom': this.cfg.filterFunction
        };

        this.filterMatcher = this.filterMatchers[this.cfg.filterMatchMode];
    },

    /**
     * A filter function that takes a value and a search and returns true if the value starts with the search term.
     * @param {string} value Value to be filtered 
     * @param {string} filter Filter or search term to apply.
     * @return {boolean} `true` if the given value starts with the search term, or `false` otherwise. 
     */
    startsWithFilter: function(value, filter) {
        return value.indexOf(filter) === 0;
    },

    /**
     * A filter function that takes a value and a search and returns true if the value contains the search term.
     * @param {string} value Value to be filtered 
     * @param {string} filter Filter or search term to apply.
     * @return {boolean} `true` if the given value contains the search term, or `false` otherwise. 
     */
    containsFilter: function(value, filter) {
        return value.indexOf(filter) !== -1;
    },

    /**
     * A filter function that takes a value and a search and returns true if the value ends with the search term.
     * @param {string} value Value to be filtered 
     * @param {string} filter Filter or search term to apply.
     * @return {boolean} `true` if the given value ends with the search term, or `false` otherwise. 
     */
    endsWithFilter: function(value, filter) {
        return value.indexOf(filter, value.length - filter.length) !== -1;
    },

    /**
     * Filters the overlay panel with the additional buttons actions, leaving only the buttons that match the given
     * search term.
     * @param {string} value Search term for filtering. 
     */
    filter: function(value) {
        var filterValue = PrimeFaces.trim(value).toLowerCase();

        if(filterValue === '') {
            this.menuitems.filter(':hidden').show();
            this.menuitemContainer.children('.ui-widget-header').show();
            this.menuitemContainer.children('.ui-separator').show();
        }
        else {
            for(var i = 0; i < this.menuitems.length; i++) {
                var menuitem = this.menuitems.eq(i),
                itemLabel = menuitem.find('.ui-menuitem-text').text().toLowerCase();

                /* for keyboard support */
                menuitem.removeClass('ui-state-hover');
                
                if(this.filterMatcher(itemLabel, filterValue))
                    menuitem.show();
                else
                    menuitem.hide();

            }

            //groups
            var groupHeaders = this.menuitemContainer.children('.ui-widget-header');
            for(var g = 0; g < groupHeaders.length; g++) {
                var group = groupHeaders.eq(g);

                if(g === (groupHeaders.length - 1)) {
                    if(group.nextAll('.ui-submenu-child').filter(':visible').length === 0)
                        group.hide();
                    else
                        group.show();
                }
                else {
                    if(group.nextUntil('.ui-widget-header').filter(':visible').length === 0)
                        group.hide();
                    else
                        group.show();
                }
            }
            
            var separators = this.menuitemContainer.children('.ui-separator');
            for(var s = 0; s < separators.length; s++) {
                var separator = separators.eq(s);
                if(separator.nextAll().filter(':visible').length === 0 || separator.prevAll().filter(':visible').length === 0) {
                    separator.hide();
                }
                else {
                    separator.show();
                }
            }
        }
        
        this.alignPanel();
    },

    /**
     * Shows the overlay panel with the additional buttons actions.
     * @private
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.menu.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.bindPanelEvents();

                    $this.jq.attr('aria-expanded', true);

                    if ($this.cfg.filter) {
                        $this.filterInput.trigger('focus');
                    }
                    else {
                        $this.menuButton.trigger('focus');
                    }
                }
            });
        }
    },

    /**
     * Hides the overlay panel with the additional buttons actions.
     * @private
     */
    hide: function() {
        if (this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.jq.attr('aria-expanded', false);
                    $this.menuitems.filter('.ui-state-hover').removeClass('ui-state-hover');
                    $this.menuButton.removeClass('ui-state-focus');
                }
            });
        }
    },

    /**
     * Align the overlay panel with the additional buttons actions.
     */
    alignPanel: function() {
        this.menu.css({ left:'', top:'', 'transform-origin': 'center top' });

        if(this.menu.parent().is(this.jq)) {
            this.menu.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px'
            });
        }
        else {
            this.menu.position({
                my: 'left top'
                ,at: 'left bottom'
                ,of: this.button
                ,collision: 'flipfit'
                ,using: function(pos, directions) {
                    $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                }
            });
        }
    }

});

/**
 * __PrimeFaces ThemeSwitcher Widget__
 * 
 * ThemeSwitcher enables switching PrimeFaces themes on the fly with no page refresh.
 * 
 * @interface {PrimeFaces.widget.ThemeSwitcherCfg} cfg The configuration for the {@link  ThemeSwitcher| ThemeSwitcher 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.SelectOneMenuCfg} cfg
 * 
 * @prop {string} cfg.appendTo The search expression for the element to which the overlay panel should be appended.
 */
PrimeFaces.widget.ThemeSwitcher = PrimeFaces.widget.SelectOneMenu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        var $this = this;
        this.input.on('change', function() {
            PrimeFaces.changeTheme($this.getSelectedValue());
        });
    }
});

/**
 * __PrimeFaces MultiSelectListbox Widget__
 * 
 * MultiSelectListbox is used to select an item from a collection of listboxes that are in parent-child relationship.
 * 
 * @prop {JQuery} root The DOM element for the root box with no children.
 * @prop {JQuery} items The DOM elements in all boxes that can be selected.
 * @prop {JQuery} input The hidden input field storing the selected value.
 * 
 * @interface {PrimeFaces.widget.MultiSelectListboxCfg} cfg The configuration for the {@link  MultiSelectListbox| MultiSelectListbox 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.disabled If true, disables the component.
 * @prop {boolean} cfg.showHeaders Displays label of a group at header section of the children items.
 * @prop {string} cfg.effect Effect to use when showing a group of items.
 */
PrimeFaces.widget.MultiSelectListbox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
       this._super(cfg);

       this.root = this.jq.children('div.ui-multiselectlistbox-listcontainer');
       this.items = this.jq.find('li.ui-multiselectlistbox-item');
       this.input = $(this.jqId + '_input');
       this.cfg.disabled = this.jq.hasClass('ui-state-disabled');

       if(!this.cfg.disabled) {
           this.bindEvents();
       }

       var value = this.input.val();
       if(value !== '') {
           this.preselect(value);
       }
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
       var $this = this;

       this.items.on('mouseover.multiSelectListbox', function() {
           var item = $(this);

           if(!item.hasClass('ui-state-highlight'))
               $(this).addClass('ui-state-hover');
       })
       .on('mouseout.multiSelectListbox', function() {
           var item = $(this);

           if(!item.hasClass('ui-state-highlight'))
               $(this).removeClass('ui-state-hover');
       })
       .on('click.multiSelectListbox', function() {
           var item = $(this);

           if(!item.hasClass('ui-state-highlight')){
               $this.showOptionGroup(item);
           }
       });
    },

    /**
     * Removes some of the event listener that were registered by `bindEvents`. Called when this widget is disabled.
     * @private
     */
    unbindEvents: function() {
       this.items.off('mouseover.multiSelectListbox mouseout.multiSelectListbox click.multiSelectListbox');
    },

    /**
     * Shows the given box with a group of options.
     * @private
     * @param {JQuery} item The box to be shown.
     */
    showOptionGroup: function(item) {
       item.addClass('ui-state-highlight').removeClass('ui-state-hover').siblings().filter('.ui-state-highlight').removeClass('ui-state-highlight');
       item.closest('.ui-multiselectlistbox-listcontainer').nextAll().remove();
       this.input.val(item.attr('data-value'));

       var childItemsContainer = item.children('ul');

       if(childItemsContainer.length) {
           var groupContainer = $('<div class="ui-multiselectlistbox-listcontainer" style="display:none"></div>');
           childItemsContainer.clone(true).appendTo(groupContainer).addClass('ui-multiselectlistbox-list ui-inputfield ui-widget-content').removeClass('ui-helper-hidden');

           if(this.cfg.showHeaders) {
               groupContainer.prepend('<div class="ui-multiselectlistbox-header ui-widget-header ui-corner-top">' + PrimeFaces.escapeHTML(item.children('span').text()) + '</div>')
                       .children('.ui-multiselectlistbox-list').addClass('ui-corner-bottom');
           } else {
               groupContainer.children().addClass('ui-corner-all');
           }

           this.jq.append(groupContainer);

           if(this.cfg.effect)
               groupContainer.show(this.cfg.effect);
           else
               groupContainer.show();
       }
       else {
           this.triggerChange();
       }
    },

    /**
     * Enables this list box so that the user can select an item.
     */
    enable: function() {
       if(this.cfg.disabled) {
           this.cfg.disabled = false;
           this.jq.removeClass('ui-state-disabled');
           this.bindEvents();
       }

    },

    /**
     * Disabled this list box so that the user cannot select items anymore.
     */
    disable: function() {
       if(!this.cfg.disabled) {
           this.cfg.disabled = true;
           this.jq.addClass('ui-state-disabled');
           this.unbindEvents();
           this.root.nextAll().remove();
       }
    },

    /**
     * Selects the item with the given value, expanding and showing all parent boxes as neccessary.
     * @param {string} value Value of the item to be shown. 
     */
    preselect: function(value) {
        var $this = this,
        item = this.items.filter('[data-value="' + $.escapeSelector(value)+'"]');

        if(item.length === 0) {
            return;
        }

        var ancestors = item.parentsUntil('.ui-multiselectlistbox-list'),
        selectedIndexMap = [];

        for(var i = (ancestors.length - 1); i >= 0; i--) {
            var ancestor = ancestors.eq(i);

            if(ancestor.is('li')) {
                selectedIndexMap.push(ancestor.index());
            }
            else if(ancestor.is('ul')) {
                var groupContainer = $('<div class="ui-multiselectlistbox-listcontainer" style="display:none"></div>');
                ancestor.clone(true).appendTo(groupContainer).addClass('ui-multiselectlistbox-list ui-inputfield ui-widget-content ui-corner-all').removeClass('ui-helper-hidden');

                if(this.cfg.showHeaders) {
                   groupContainer.prepend('<div class="ui-multiselectlistbox-header ui-widget-header ui-corner-top">' + PrimeFaces.escapeHTML(ancestor.prev('span').text()) + '</div>')
                           .children('.ui-multiselectlistbox-list').addClass('ui-corner-bottom').removeClass('ui-corner-all');
                }

                $this.jq.append(groupContainer);
            }
        }

        //highlight item
        var lists = this.jq.children('div.ui-multiselectlistbox-listcontainer'),
        clonedItem = lists.find(' > ul.ui-multiselectlistbox-list > li.ui-multiselectlistbox-item').filter('[data-value="' + $.escapeSelector(value) + '"]');
        clonedItem.addClass('ui-state-highlight');

        //highlight ancestors
        for(var i = 0; i < selectedIndexMap.length; i++) {
            lists.eq(i).find('> .ui-multiselectlistbox-list > li.ui-multiselectlistbox-item').eq(selectedIndexMap[i]).addClass('ui-state-highlight');
        }

        $this.jq.children('div.ui-multiselectlistbox-listcontainer:hidden').show();
    },

    /**
     * Triggers the change behavior, invoked after an item was selected or deselected.
     * @private
     */
    triggerChange: function () {
        this.callBehavior('change');
    }
});

/**
 * __PrimeFaces Growl Widget__
 *
 * Growl is based on the Mac’s growl notification widget and used to display FacesMessages in an overlay.
 *
 * @interface {PrimeFaces.widget.GrowlCfg} cfg The configuration for the {@link  Growl| Growl 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.escape `true` to treat the message's summary and details as plain text, `false` to treat them as
 * an HTML string.
 * @prop {boolean} cfg.keepAlive Defines if previous messages should be kept on a new message is shown.
 * @prop {number} cfg.life Duration in milliseconds to display non-sticky messages.
 * @prop {PrimeFaces.FacesMessage[]} cfg.msgs List of messages that are shown initially when the widget is loaded or
 * refreshed.
 * @prop {boolean} cfg.sticky Specifies if the message should stay instead of hidden automatically.
 */
PrimeFaces.widget.Growl = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        //create container
        this.jq = $('<div id="' + this.id + '_container" class="ui-growl ui-widget" aria-live="polite"></div>');
        this.jq.appendTo($(document.body));

        //render messages
        this.show(this.cfg.msgs);
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
    	this.cfg = cfg;
        this.show(cfg.msgs);

        this.removeScriptElement(this.id);
    },

    /**
     * Appends a message to the current displayed messages.
     *
     * @param {PrimeFaces.FacesMessage} msg A message to translate into an HTML element.
     */
    add: function(msg) {
        this.renderMessage(msg);
    },

    /**
     * Appends all messages to the current displayed messages.
     *
     * @param {PrimeFaces.FacesMessage[]} msgs The messages to translate into HTML elements.
     */
    addAll: function(msgs) {
        var $this = this;
        $.each(msgs, function(index, msg) {
            $this.renderMessage(msg);
        });
    },

    /**
     * Displays the given messages in the growl window represented by this growl widget.
     *
     * @param {PrimeFaces.FacesMessage[]} msgs Messages to display in this growl
     */
    show: function(msgs) {
        var $this = this;

        this.jq.css('z-index', PrimeFaces.nextZindex());

        if(!this.cfg.keepAlive) {
            //clear previous messages
            this.removeAll();
        }

        $.each(msgs, function(index, msg) {
            $this.renderMessage(msg);
        });
    },

    /**
     * Removes all growl messages that are currently displayed.
     */
    removeAll: function() {
        this.jq.children('div.ui-growl-item-container').remove();
    },

    /**
     * Creates the HTML elements for the given faces message, and adds it to the DOM.
     * @private
     * @param {PrimeFaces.FacesMessage} msg A message to translate into an HTML element.
     */
    renderMessage: function(msg) {
        var markup = '<div class="ui-growl-item-container ui-state-highlight ui-corner-all ui-helper-hidden ui-shadow ui-growl-' + msg.severity + '">';
        markup += '<div role="alert" class="ui-growl-item">';
        markup += '<div class="ui-growl-icon-close ui-icon ui-icon-closethick" style="display:none"></div>';
        markup += '<span class="ui-growl-image ui-growl-image-' + msg.severity + '" ></span>';
        if (msg.severityText) {
            // GitHub #5153 for screen readers
            markup += '<span class="ui-growl-severity ui-helper-hidden-accessible">' + msg.severityText + '</span>';
        }
        markup += '<div class="ui-growl-message">';
        markup += '<span class="ui-growl-title"></span>';
        markup += '<p></p>';
        markup += '</div><div style="clear: both;"></div></div></div>';

        var message = $(markup),
        summaryEL = message.find('span.ui-growl-title'),
        detailEL = summaryEL.next();

        if(this.cfg.escape) {
            summaryEL.text(msg.summary);
            detailEL.text(msg.detail);
        }
        else {
            summaryEL.html(msg.summary);
            detailEL.html(msg.detail);
        }

        this.bindEvents(message);

        message.appendTo(this.jq).fadeIn();
    },

    /**
     * Sets up all event listeners for the given message, such as for closing the message when the close icon clicked.
     * @private
     * @param {JQuery} message The message for which to set up the event listeners
     */
    bindEvents: function(message) {
        var $this = this,
        sticky = this.cfg.sticky;

        message.on("mouseover", function() {
            var msg = $(this);

            //visuals
            if(!msg.is(':animated')) {
                msg.find('div.ui-growl-icon-close:first').show();
            }

            // clear hide timeout on mouseover
            if(!sticky) {
                clearTimeout(msg.data('timeout'));
            }
        })
        .on("mouseout", function() {
            //visuals
            $(this).find('div.ui-growl-icon-close:first').hide();

            // setup hide timeout again after mouseout
            if(!sticky) {
                $this.setRemovalTimeout(message);
            }
        });

        //remove message on click of close icon
        message.find('div.ui-growl-icon-close').on("click", function() {
            $this.removeMessage(message);

            //clear timeout if removed manually
            if(!sticky) {
                clearTimeout(message.data('timeout'));
            }
        });

        //hide the message after given time if not sticky
        if(!sticky) {
            this.setRemovalTimeout(message);
        }
    },

    /**
     * Removes the given message from the screen, if it is currently displayed.
     * @param {JQuery} message The message to remove, an HTML element with the class `ui-growl-item-container`.
     */
    removeMessage: function(message) {
        message.fadeTo('normal', 0, function() {
            message.slideUp('normal', 'easeInOutCirc', function() {
                message.remove();
            });
        });
    },

    /**
     * Starts a timeout that removes the given message after a certain delay (as defined by this widget's
     * configuration).
     * @private
     * @param {JQuery} message The message to remove, an HTML element with the class `ui-growl-item-container`.
     */
    setRemovalTimeout: function(message) {
        var $this = this;

        var timeout = setTimeout(function() {
            $this.removeMessage(message);
        }, this.cfg.life);

        message.data('timeout', timeout);
    }
});
/**
 * __PrimeFaces Inplace Widget__
 *
 * Inplace provides easy inplace editing and inline content display. Inplace
 * consists of two members, a display element that is the initially clickable
 * label and an inline element that is the hidden content which is displayed
 * when the display element is toggled.
 *
 * @typedef {"fade" | "none" | "slide"} PrimeFaces.widget.Inplace.EffectType Available effect types for the transition
 * between the display and the inline content of the inline widget.
 *
 * @prop {JQuery} content The DOM element with the container of the label that is shown when the content or inplace
 * editor is hidden.
 * @prop {JQuery} display The DOM element with the container of the content or inplace editor that is shown when this
 * inline widget is toggled.
 * @prop {JQuery} editor The DOM element with the inplace editor, if one exists.
 *
 * @interface {PrimeFaces.widget.InplaceCfg} cfg The configuration for the {@link  Inplace| Inplace 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.disabled Whether this inplace widget is disabled. If disabled, switching to the content or
 * inplace editor is not possible.
 * @prop {string} cfg.formId The ID of the form that is used for AJAX requests. Usually the containing form.
 * @prop {boolean} cfg.editor `true` to add save and cancel buttons to the inline content, or `false` otherwise. Usually
 * used when the inline content is a form field.
 * @prop {PrimeFaces.widget.Inplace.EffectType} cfg.effect Effect to be used when toggling.
 * @prop {number} cfg.effectSpeed Speed of the effect in milliseconds.
 * @prop {string} cfg.event Name of the client side event to display inline content.
 * @prop {boolean} cfg.toggleable Defines if inplace content is toggleable or not.
 */
PrimeFaces.widget.Inplace = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.display = $(this.jqId + '_display');
        this.content = $(this.jqId + '_content');
        this.cfg.formId = this.jq.parents('form:first').attr('id');

        var $this = this;

        if(!this.cfg.disabled) {

            if(this.cfg.toggleable) {
                this.display.on(this.cfg.event, function(){
                    $this.show();
                });

                this.display.on("mouseover", function(){
                    $(this).toggleClass("ui-state-highlight");
                }).on("mouseout", function(){
                    $(this).toggleClass("ui-state-highlight");
                });
            }
            else {
                this.display.css('cursor', 'default');
            }

            if(this.cfg.editor) {
                this.cfg.formId = this.jq.parents('form:first').attr('id');

                this.editor = $(this.jqId + '_editor');

                var saveButton = this.editor.children('.ui-inplace-save'),
                    cancelButton = this.editor.children('.ui-inplace-cancel');

                PrimeFaces.skinButton(saveButton).skinButton(cancelButton);

                saveButton.on("click", function(e) {
                    $this.save(e);
                });
                cancelButton.on("click", function(e) {
                    $this.cancel(e);
                });
            }

            /* to enter space in inplace input within multi-selection dataTable */
            this.content.find('input:text,textarea').on('keydown.inplace-text', function(e) {
                var keyCode = $.ui.keyCode;

                if(e.which === keyCode.SPACE) {
                    e.stopPropagation();
                }
            });
        }
    },

    /**
     * Switches to editing mode and displays the inplace editor.
     */
    show: function() {
        this.toggle(this.content, this.display);
    },

    /**
     * Leaves editing mode and hides the inplace editor.
     */
    hide: function() {
        this.toggle(this.display, this.content);
    },

    /**
     * Hides the label and shows the inline content or inplace editor; or vice versa.
     * @private
     * @param {JQuery} elToShow Element to show, either the label or the inplace editor.
     * @param {JQuery} elToHide Element to hide, either the label or the inplace editor.
     */
    toggle: function(elToShow, elToHide) {
        var $this = this;

        if(this.cfg.effect === 'fade') {
            elToHide.fadeOut(this.cfg.effectSpeed, function() {
                elToShow.fadeIn($this.cfg.effectSpeed);
                $this.postShow();
            });
        }
        else if(this.cfg.effect === 'slide') {
            elToHide.slideUp(this.cfg.effectSpeed, function() {
                elToShow.slideDown($this.cfg.effectSpeed);
                $this.postShow();
            });
        }
        else if(this.cfg.effect === 'none') {
            elToHide.hide();
            elToShow.show();
            $this.postShow();
        }
    },

    /**
     * Callback that is invoked when the inline content or inplace editor is shown or hidden. Puts focus on the
     * appropriate element and makes sure the inline content is rendered correctly.
     * @private
     */
    postShow: function() {
        this.content.find('input:text,textarea').filter(':visible:enabled:first').trigger('focus').trigger('select');

        PrimeFaces.invokeDeferredRenders(this.id);
    },

    /**
     * Fetches the display element, which is the container with the label or description shown when the inline content
     * is not displayed.
     * @return {JQuery} The display element or label when the editor is not shown.
     */
    getDisplay: function() {
        return this.display;
    },

    /**
     * Fetches the content element, which is the container element with the inline content or inplace editor.
     * @return {JQuery} The content element with the inplace editor.
     */
    getContent: function() {
        return this.content;
    },

    /**
     * When an inplace editor exists and it is currently active: saves the content of the editor and hides the inplace
     * editor.
     * @param {JQuery.TriggeredEvent} [e] The (click) event which triggered the saving. Currently unused.
     */
    save: function(e) {
        var options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.cfg.formId
        };

        if(this.hasBehavior('save')) {
            this.callBehavior('save', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When an inplace editor exists and it is currently active: discard changes that were made and hides the inplace
     * editor.
     * @param {JQuery.TriggeredEvent} [e] The (click) event which triggered the cancellation. Currently unused.
     */
    cancel: function(e) {
        var options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.cfg.formId
        };

        options.params = [
            {name: this.id + '_cancel', value: true}
        ];

        if(this.hasBehavior('cancel')) {
            this.callBehavior('cancel', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    }

});

/**
 * __PrimeFaces LightBox Widget__
 * 
 * @typedef {"iframe" | "image" | "inlne"} PrimeFaces.widget.LightBox.ContentMode Type of the content that is shown in
 * the lightbox.
 * 
 * @typedef PrimeFaces.widget.LightBox.OnHideCallback Client-side callback invoked when the lightbox is hidden. See also
 * {@link LightBoxCfg.onHide}.
 * @this {PrimeFaces.widget.LightBox} PrimeFaces.widget.LightBox.OnHideCallback 
 * 
 * @typedef PrimeFaces.widget.LightBox.OnShowCallback Client-side callback invoked when the lightbox is shown. See also
 * {@link LightBoxCfg.onShow}.
 * @this {PrimeFaces.widget.LightBox} PrimeFaces.widget.LightBox.OnShowCallback 
 * 
 * @typedef {() => void} PrimeFaces.widget.LightBox.OnShowHandlersCallback List of registered callback handlers for when
 * the lightbox is shown. See also {@link LightBox.onshowHandlers}.
 * 
 * @interface {PrimeFaces.widget.LightBox.UrlSettings} UrlSettings Settings for showing an URL in an iframe inside
 * the lightbox.
 * @prop {number} [UrlSettings.height] Height of the iframe in pixels.
 * @prop {string} UrlSettings.src URL to show in an iframe.
 * @prop {string} [UrlSettings.title] Title text to show below the iframe.
 * @prop {number} [UrlSettings.width] Width of the iframe in pixels.
 * 
 * @prop {JQuery} caption The DOM element for the caption container below the lightbox.
 * @prop {JQuery} captionText The DOM element for the caption text below the lightbox.
 * @prop {JQuery} closeIcon The DOM element for the close icon to hide the lightbox.
 * @prop {JQuery} content The DOM element for the content of the lightbox
 * @prop {JQuery} contentWrapper The DOM element for the content container of the lightbox-
 * @prop {number} current Index of the slide currently being shown.
 * @prop {JQuery} iframe The DOM element for the iframe, if `mode` is set to `iframe`.
 * @prop {boolean} iframeLoaded Whether the iframe was already loaded.
 * @prop {JQuery} imageDisplay The DOM element for the image, if `mode` is set to `image`.
 * @prop {JQuery} inline The DOM element for the inline content element, if `mode` is set to `inline`.
 * @prop {JQuery} links The DOM element for the links in the inline content, if `mode` is set to `inline`.
 * @prop {JQuery} navigators The DOM element for the arrow buttons for switching to the previous or next slide. 
 * @prop {PrimeFaces.widget.LightBox.OnShowHandlersCallback[]} onshowHandlers List of registered callback handlers for
 * when the lightbox is shown.
 * @prop {JQuery} panel The DOM element for the entire lightbox overlay panel.
 * 
 * @interface {PrimeFaces.widget.LightBoxCfg} cfg The configuration for the {@link  LightBox| LightBox 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 {string} cfg.appendTo Selector for the element to which the overlay lightbox panel is appended.
 * @prop {number} cfg.height Height of the overlay in iframe mode.
 * @prop {string} cfg.iframeTitle Title of the iframe element.
 * @prop {PrimeFaces.widget.LightBox} cfg.mode The type of content that is shown in the lightbox.
 * @prop {PrimeFaces.widget.LightBox.OnHideCallback} cfg.onHide Client-side callback invoked when the lightbox is
 * hidden.
 * @prop {PrimeFaces.widget.LightBox.OnShowCallback} cfg.onShow Client-side callback invoked when the lightbox is
 * shown.
 * @prop {boolean} cfg.visible Whether the lightbox is initially visible.
 * @prop {number} cfg.width Width of the overlay in iframe mode.
 */
PrimeFaces.widget.LightBox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        // the dynamic overlay must be appended to the body
        cfg.appendTo = '@(body)';

        this._super(cfg);

        this.links = this.jq.children(':not(.ui-lightbox-inline)');

        this.createPanel();

        if(this.cfg.mode === 'image') {
            this.setupImaging();
        } else if(this.cfg.mode === 'inline') {
            this.setupInline();
        } else if(this.cfg.mode === 'iframe') {
            this.setupIframe();
        }

        this.bindCommonEvents();

        if(this.cfg.visible) {
            this.links.eq(0).trigger("click");
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        PrimeFaces.utils.removeDynamicOverlay(this, this.panel, this.id + '_panel', $(document.body));

        this._super(cfg);
    },

    /**
     * Creates the DOM elements for the lightbox panel.
     * @private
     */
    createPanel: function() {
        this.panel = $('<div id="' + this.id + '_panel" class="ui-lightbox ui-widget ui-helper-hidden ui-corner-all ui-shadow">'
            + '<div class="ui-lightbox-content-wrapper">'
            + '<a class="ui-state-default ui-lightbox-nav-left ui-corner-right ui-helper-hidden"><span class="ui-icon ui-icon-carat-1-w">go</span></a>'
            + '<div class="ui-lightbox-content ui-corner-all"></div>'
            + '<a class="ui-state-default ui-lightbox-nav-right ui-corner-left ui-helper-hidden"><span class="ui-icon ui-icon-carat-1-e">go</span></a>'
            + '</div>'
            + '<div class="ui-lightbox-caption ui-widget-header"><span class="ui-lightbox-caption-text"></span>'
            + '<a class="ui-lightbox-close ui-corner-all" href="#"><span class="ui-icon ui-icon-closethick"></span></a><div style="clear:both"></div></div>'
            + '</div>');

        PrimeFaces.utils.registerDynamicOverlay(this, this.panel, this.id + '_panel');

        this.contentWrapper = this.panel.children('.ui-lightbox-content-wrapper');
        this.content = this.contentWrapper.children('.ui-lightbox-content');
        this.caption = this.panel.children('.ui-lightbox-caption');
        this.captionText = this.caption.children('.ui-lightbox-caption-text');
        this.closeIcon = this.caption.children('.ui-lightbox-close');
    },

    /**
     * Sets up the DOM elements and events handlers for showing images in the lightbox
     * @private
     */
    setupImaging: function() {
        var $this = this;

        this.content.append('<img class="ui-helper-hidden"></img>');
        this.imageDisplay = this.content.children('img');
        this.navigators = this.contentWrapper.children('a');

        this.imageDisplay.on('load', function() {
            var image = $(this);

            $this.scaleImage(image);

            //coordinates to center overlay
            var leftOffset = ($this.panel.width() - image.width()) / 2,
            topOffset = ($this.panel.height() - image.height()) / 2;

            //resize content for new image
            $this.content.removeClass('ui-lightbox-loading');
            $this.content.stop().animate({ width: image.width(), height: image.height() },
                500,
                function() {
                    //show image
                    image.fadeIn();
                    $this.showNavigators();
                    $this.caption.slideDown();
                });

            $this.panel.stop().animate({ left: '+=' + leftOffset, top: '+=' + topOffset}, 500);
        });

        this.navigators.on("mouseover", function() {
            $(this).addClass('ui-state-hover');
        })
        .on("mouseout", function() {
            $(this).removeClass('ui-state-hover');
        })
        .on("click", function(e) {
            var nav = $(this);

            $this.hideNavigators();

            if(nav.hasClass('ui-lightbox-nav-left')) {
                var index = $this.current == 0 ? $this.links.length - 1 : $this.current - 1;

                $this.links.eq(index).trigger('click');
            }
            else {
                var index = $this.current == $this.links.length - 1 ? 0 : $this.current + 1;

                $this.links.eq(index).trigger('click');
            }

            e.preventDefault();
        });

        this.links.on("click", function(e) {
            var link = $(this);

            if($this.isHidden()) {
                $this.content.addClass('ui-lightbox-loading').width(32).height(32);
                $this.show();
            }
            else {
                $this.imageDisplay.stop().fadeOut(function() {
                    //clear for onload scaling
                    $(this).css({
                        'width': 'auto'
                        ,'height': 'auto'
                    });

                    $this.content.addClass('ui-lightbox-loading');
                });

                $this.caption.stop().slideUp();
            }

            clearTimeout(this.timeout);
            this.timeout = setTimeout(function() {
                $this.imageDisplay.attr('src', link.attr('href'));
                $this.current = link.index();

                var title = link.attr('title');
                if(title) {
                    $this.captionText.text(title);
                }
            }, 1000);

            e.preventDefault();
        });
    },

    /**
     * Scales the given image so that it fits the lightbox viewport.
     * @param {JQuery} image An image to be scaled.
     * @private
     */
    scaleImage: function(image) {
        var win = $(window),
        winWidth = win.width(),
        winHeight = win.height(),
        imageWidth = image.width(),
        imageHeight = image.height(),
        ratio = imageHeight / imageWidth;

        if(imageWidth >= winWidth && ratio <= 1){
            imageWidth = winWidth * 0.75;
            imageHeight = imageWidth * ratio;
        }
        else if(imageHeight >= winHeight){
            imageHeight = winHeight * 0.75;
            imageWidth = imageHeight / ratio;
        }

        image.css({
            'width':imageWidth + 'px'
            ,'height':imageHeight + 'px'
        });
    },

    /**
     * Sets up the DOM elements and events handlers for inline content such as videos in the lightbox.
     * @private
     */
    setupInline: function() {
        this.inline = this.jq.children('.ui-lightbox-inline');
        this.inline.appendTo(this.content).show();
        var $this = this;

        this.links.on("click", function(e) {
            $this.show();

            var title = $(this).attr('title');
            if(title) {
                $this.captionText.text(title);
                $this.caption.stop().slideDown();
            }

            e.preventDefault();
        });
    },

    /**
     * Sets up the DOM elements and events handlers for showing an external page inside an iframe in the lightbox.
     * @private
     */
    setupIframe: function() {
        var $this = this;
        this.iframeLoaded = false;
        this.cfg.width = this.cfg.width||'640px';
        this.cfg.height = this.cfg.height||'480px';

        this.iframe = $('<iframe frameborder="0" style="width:' + this.cfg.width + ';height:'
                        + this.cfg.height + ';border:0 none; display: block;"></iframe>').appendTo(this.content);

        if(this.cfg.iframeTitle) {
            this.iframe.attr('title', this.cfg.iframeTitle);
        }

        this.links.on("click", function(e) {
            if(!$this.iframeLoaded) {
                $this.content.addClass('ui-lightbox-loading').css({
                    width: $this.cfg.width
                    ,height: $this.cfg.height
                });
                $this.show();

                $this.iframe.on('load', function() {
                                $this.iframeLoaded = true;
                                $this.content.removeClass('ui-lightbox-loading');
                            })
                            .attr('src', $this.links.eq(0).attr('href'));
            }
            else {
                $this.show();
            }

            var title = $this.links.eq(0).attr('title');
            if(title) {
                $this.captionText.text(title);
                $this.caption.slideDown();
            }

            e.preventDefault();
        });
    },

    /**
     * Sets up some common event handlers required independent of the content shown in the lightbox.
     * @private
     */
    bindCommonEvents: function() {
        var $this = this;

        this.closeIcon.on("mouseover", function() {
            $(this).addClass('ui-state-hover');
        })
        .on("mouseout", function() {
            $(this).removeClass('ui-state-hover');
        });

        this.closeIcon.on("click", function(e) {
            $this.hide();
            e.preventDefault();
        });

        var hideEvent = PrimeFaces.env.ios ? 'touchstart' : 'click';
        PrimeFaces.utils.registerHideOverlayHandler(this, hideEvent + '.' + this.id + '_hide', $this.panel,
            function() { return $this.links.add($this.closeIcon); },
            function(e, eventTarget) {
                if(!($this.panel.is(eventTarget) || $this.panel.has(eventTarget).length > 0)) {
                    e.preventDefault();
                    $this.hide();
                }
            });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.panel, function() {
            $(document.body).children('.ui-widget-overlay').css({
                'width': $(document).width() + 'px'
                ,'height': $(document).height() + 'px'
            });
        });
    },

    /**
     * Brings up this lightbox and shows it to the user.
     */
    show: function() {
        this.center();

        this.panel.css('z-index', PrimeFaces.nextZindex()).show();

        if(!PrimeFaces.utils.isModalActive(this.id)) {
            this.enableModality();
        }

        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }
    },

    /**
     * Closes this lightbox and hides it from view.
     */
    hide: function() {
        this.panel.fadeOut();
        this.disableModality();
        this.caption.hide();

        if(this.cfg.mode == 'image') {
            this.imageDisplay.hide().attr('src', '').removeAttr('style');
            this.hideNavigators();
        }

        if(this.cfg.onHide) {
            this.cfg.onHide.call(this);
        }
    },

    /**
     * Centers this lightbox so that is moved to the center of the browser viewport.
     */
    center: function() {
        var win = $(window),
        left = (win.width() / 2 ) - (this.panel.width() / 2),
        top = (win.height() / 2 ) - (this.panel.height() / 2);

        this.panel.css({
            'left': left + 'px',
            'top': top + 'px'
        });
    },

    /**
     * Makes this  lightbox a modal dialog so that the user cannot interact with other content on the page.
     */
    enableModality: function() {
        PrimeFaces.utils.addModal(this, this.panel);
    },

    /**
     * Makes this  lightbox a non-modal dialog so that the user can interact with other content on the page.
     */
    disableModality: function() {
        PrimeFaces.utils.removeModal(this, this.panel);
    },

    /**
     * Displays the navigator buttons for switching to the previous or next slide.
     */
    showNavigators: function() {
        this.navigators.zIndex(this.imageDisplay.zIndex() + 1).show();
    },

    /**
     * Hides the navigator buttons for switching to the previous or next slide.
     */
    hideNavigators: function() {
        this.navigators.hide();
    },

    /**
     * Adds a callback that is invoked when the lightbox is displayed.
     * @param {() => void} fn A callback that is invoked when the lightbox is shown. 
     * @private
     */
    addOnshowHandler: function(fn) {
        this.onshowHandlers.push(fn);
    },

    /**
     * Checks whether this light is currently being displayed.
     * @return {boolean} `true` if this lightbox is currently hidden, or `false` otherwise.
     */
    isHidden: function() {
        return this.panel.is(':hidden');
    },

    /**
     * Shows the given URL in an IFRAME inside this lightbox.
     * @param {PrimeFaces.widget.LightBox.UrlSettings} opt Options for how the URL is shown.
     */
    showURL: function(opt) {
        if(opt.width)
            this.iframe.attr('width', opt.width);
        if(opt.height)
            this.iframe.attr('height', opt.height);

        this.iframe.attr('src', opt.src);

        this.captionText.text(opt.title||'');
        this.caption.slideDown();

        this.show();
    }

});

/**
 * __PrimeFaces Menu Widget__
 * 
 * Base class for the different menu widets, such as the `PlainMenu` or the `TieredMenu`.
 * 
 * @prop {JQuery} keyboardTarget The DOM element for the form element that can be targeted via arrow or tab keys. 
 * @prop {boolean} itemMouseDown `true` if a menu item was clicked and the mouse button is still pressed.
 * @prop {JQuery} trigger DOM element which triggers this menu.
 * 
 * @interface {PrimeFaces.widget.MenuCfg} cfg The configuration for the {@link  Menu| Menu 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 {string} cfg.appendTo Search expression for the element to which the menu overlay is appended.
 * @prop {string} cfg.at Defines which position on the target element to align the positioned element against
 * @prop {string} cfg.collision When the positioned element overflows the window in some direction, move it to an
 * alternative position.
 * @prop {string} cfg.my Defines which position on the element being positioned to align with the target element.
 * @prop {boolean} cfg.overlay `true` if this menu is displayed as an overlay, or `false` otherwise.
 * @prop {JQueryUI.JQueryPositionOptions} cfg.pos Describes how to align this menu.
 * @prop {string} cfg.trigger ID of the event which triggers this menu.
 * @prop {string} cfg.triggerEvent Event which triggers this menu.
 */
PrimeFaces.widget.Menu = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        if(this.cfg.overlay) {
            this.initOverlay();
        }

        this.keyboardTarget = this.jq.children('.ui-helper-hidden-accessible');
    },

    /**
     * Initializes the overlay. Finds the element to which to append this menu and appends it to that element.
     * @protected
     */
    initOverlay: function() {
        var $this = this;

        this.jq.addClass('ui-menu-overlay');

        this.cfg.trigger = this.cfg.trigger.replace(/\\\\:/g,"\\:");

        this.trigger = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.trigger);

        //mark trigger and descandants of trigger as a trigger for a primefaces overlay
        this.trigger.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

        this.cfg.appendTo = '@(body)';
        PrimeFaces.utils.registerDynamicOverlay(this, this.jq, this.id);
        this.transition = PrimeFaces.utils.registerCSSTransition(this.jq, 'ui-connected-overlay');

        this.cfg.pos = {
            my: this.cfg.my,
            at: this.cfg.at,
            of: this.trigger,
            collision: this.cfg.collision || "flip",
            using: function(pos, directions) {
                $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
            }
        };

        this.trigger.off(this.cfg.triggerEvent + '.ui-menu').on(this.cfg.triggerEvent + '.ui-menu', function(e) {
            var trigger = $(this);

            if($this.jq.is(':visible')) {
                $this.hide();
            }
            else {
                $this.show();

                if(trigger.is(':button')) {
                    trigger.addClass('ui-state-focus');
                }

                e.preventDefault();
            }
        });

        //dialog support
        this.setupDialogSupport();
    },

    /**
     * Sets up all panel event listeners
     * @protected
     */
    bindPanelEvents: function() {
        var $this = this;

        //hide overlay on document click
        this.itemMouseDown = false;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.jq,
            function() { return $this.trigger; },
            function(e, eventTarget) {
                var menuItemLink = '.ui-menuitem-link:not(.ui-submenu-link, .ui-state-disabled)';

                if (eventTarget.is(menuItemLink) || eventTarget.closest(menuItemLink).length) {
                    $this.itemMouseDown = true;
                }
                else if(!($this.jq.is(eventTarget) || $this.jq.has(eventTarget).length > 0)) {
                    $this.hide(e);
                }
            });

        $(document.body).on('mouseup.' + this.id, function (e) {
            if ($this.itemMouseDown) {
                $this.hide(e);
                $this.itemMouseDown = false;
            }
        });

        //Hide overlay on resize
        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.trigger, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @protected
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }

        $(document.body).off('mouseup.' + this.id);
    },

    /**
     * Performs some setup required to make this overlay menu work with dialogs.
     * @protected
     */
    setupDialogSupport: function() {
        var dialog = this.trigger.parents('.ui-dialog:first');

        if(dialog.length == 1 && dialog.css('position') === 'fixed') {
            this.jq.css('position', 'fixed');
        }
    },

    /**
     * Shows (displays) this menu so that it becomes visible and can be interacted with.
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.jq.css('z-index', PrimeFaces.nextZindex());
                    $this.align();
                },
                onEntered: function() {
                    $this.bindPanelEvents();
                }
            });
        }
    },

    /**
     * Hides this menu so that it becomes invisible and cannot be interacted with any longer.
     */
    hide: function() {
        if (this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    if ($this.trigger && $this.trigger.is(':button')) {
                        $this.trigger.removeClass('ui-state-focus');
                    }
                }
            });
        }
    },

    /**
     * Aligns this menu as specified in its widget configuration (property `pos`).
     */
    align: function() {
        this.jq.css({left:'', top:'', 'transform-origin': 'center top'}).position(this.cfg.pos);
    }
});






/**
 * __PrimeFaces TieredMenu Widget__
 * 
 * TieredMenu is used to display nested submenus with overlays.
 * 
 * @typedef {"hover" | "click"} PrimeFaces.widget.TieredMenu.ToggleEvent Allowed event types for toggling a tiered menu.
 * 
 * @prop {JQuery} links DOM element with all links for the menu entries of this tiered menu.
 * @prop {JQuery} rootLinks DOM element with all links for the root (top-level) menu entries of this tiered menu.
 * 
 * @interface {PrimeFaces.widget.TieredMenuCfg} cfg The configuration for the {@link  TieredMenu| TieredMenu 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.MenuCfg} cfg 
 * 
 * @prop {boolean} cfg.autoDisplay Defines whether the first level of submenus will be displayed on mouseover or not.
 * When set to `false`, click event is required to display this tiered menu.
 * @prop {PrimeFaces.widget.TieredMenu.ToggleEvent} cfg.toggleEvent Event to toggle the submenus.
 */
PrimeFaces.widget.TieredMenu = PrimeFaces.widget.Menu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.toggleEvent = this.cfg.toggleEvent||'hover';
        this.links = this.jq.find('a.ui-menuitem-link:not(.ui-state-disabled)');
        this.rootLinks = this.jq.find('> ul.ui-menu-list > .ui-menuitem > .ui-menuitem-link');

        this.bindEvents();
    },

    /**
     * Sets up all event listeners required by this widget.
     * @protected
     */
    bindEvents: function() {
        this.bindItemEvents();
        this.bindKeyEvents();
        this.bindDocumentHandler();
    },

    /**
     * Sets up all event listeners for the mouse events on the menu entries (`click` / `hover`).
     * @protected
     */
    bindItemEvents: function() {
        if(this.cfg.toggleEvent === 'hover')
            this.bindHoverModeEvents();
        else if(this.cfg.toggleEvent === 'click')
            this.bindClickModeEvents();
    },

    /**
     * Sets up all event listeners when `toggleEvent` is set to `hover`.
     * @protected
     */
    bindHoverModeEvents: function() {
        var $this = this;

        this.links.on("mouseenter", function() {
            var link = $(this),
            menuitem = link.parent();

            var activeSibling = menuitem.siblings('.ui-menuitem-active');
            if(activeSibling.length === 1) {
                activeSibling.find('li.ui-menuitem-active').each(function() {
                    $this.deactivate($(this));
                });
                $this.deactivate(activeSibling);
            }

            if($this.cfg.autoDisplay||$this.active) {
                if(menuitem.hasClass('ui-menuitem-active'))
                    $this.reactivate(menuitem);
                else
                    $this.activate(menuitem);
            }
            else {
                $this.highlight(menuitem);
            }
        });

        this.rootLinks.on("click", function(e) {
            var link = $(this),
            menuitem = link.parent(),
            submenu = menuitem.children('ul.ui-menu-child');

            $this.itemClick = true;

            if(submenu.length === 1) {
                if(submenu.is(':visible')) {
                    $this.active = false;
                    $this.deactivate(menuitem);
                }
                else {
                    $this.active = true;
                    $this.highlight(menuitem);
                    $this.showSubmenu(menuitem, submenu);
                }
            }
        });

        this.links.filter('.ui-submenu-link').on("click", function(e) {
            $this.itemClick = true;
            e.preventDefault();
        });

        this.jq.find('ul.ui-menu-list').on("mouseleave", function(e) {
           if($this.activeitem) {
               $this.deactivate($this.activeitem);
           }

           e.stopPropagation();
        });
    },

    /**
     * Sets up all event listeners when `toggleEvent` is set to `click`.
     * @protected
     */
    bindClickModeEvents: function() {
        var $this = this;

        this.links.on("mouseenter", function() {
            var menuitem = $(this).parent();

            if(!menuitem.hasClass('ui-menuitem-active')) {
                menuitem.addClass('ui-menuitem-highlight').children('a.ui-menuitem-link').addClass('ui-state-hover');
            }
        })
        .on("mouseleave", function() {
            var menuitem = $(this).parent();

            if(!menuitem.hasClass('ui-menuitem-active')) {
                menuitem.removeClass('ui-menuitem-highlight').children('a.ui-menuitem-link').removeClass('ui-state-hover');
            }
        });

        this.links.filter('.ui-submenu-link').on('click.tieredMenu', function(e) {
            var link = $(this),
            menuitem = link.parent(),
            submenu = menuitem.children('ul.ui-menu-child');

            $this.itemClick = true;

            var activeSibling = menuitem.siblings('.ui-menuitem-active');
            if(activeSibling.length) {
                activeSibling.find('li.ui-menuitem-active').each(function() {
                    $this.deactivate($(this));
                });
                $this.deactivate(activeSibling);
            }

            if(submenu.length) {
                if(submenu.is(':visible')) {
                    $this.deactivate(menuitem);
                    menuitem.addClass('ui-menuitem-highlight').children('a.ui-menuitem-link').addClass('ui-state-hover');
                }
                else {
                    menuitem.addClass('ui-menuitem-active').children('a.ui-menuitem-link').removeClass('ui-state-hover').addClass('ui-state-active');
                    $this.showSubmenu(menuitem, submenu);
                }
            }

            e.preventDefault();
        }).on('mousedown.tieredMenu', function(e) {
            e.stopPropagation();
        });
    },

    /**
     * Sets up all event listners required for keyboard interactions.
     * @protected
     */
    bindKeyEvents: function() {
        //not implemented
    },

    /**
     * Registers a delegated event listener for a mouse click on a menu entry.
     * @protected
     */
    bindDocumentHandler: function() {
        var $this = this,
        clickNS = 'click.' + this.id;

        $(document.body).off(clickNS).on(clickNS, function(e) {
            if($this.itemClick) {
                $this.itemClick = false;
                return;
            }

            $this.reset();
        });
    },

    /**
     * Deactivates a menu item so that it cannot be clicked and interacted with anymore.
     * @param {JQuery} menuitem Menu item (`LI`) to deactivate.
     * @param {boolean} [animate] `true` to animate the transition to the disabled state, `false` otherwise.
     */
    deactivate: function(menuitem, animate) {
        this.activeitem = null;
        menuitem.children('a.ui-menuitem-link').removeClass('ui-state-hover ui-state-active');
        menuitem.removeClass('ui-menuitem-active ui-menuitem-highlight');

        if(animate)
            menuitem.children('ul.ui-menu-child').fadeOut('fast');
        else
            menuitem.children('ul.ui-menu-child').hide();
    },

    /**
     * Activates a menu item so that it can be clicked and interacted with.
     * @param {JQuery} menuitem Menu item (`LI`) to activate.
     */
    activate: function(menuitem) {
        this.highlight(menuitem);

        var submenu = menuitem.children('ul.ui-menu-child');
        if(submenu.length == 1) {
            this.showSubmenu(menuitem, submenu);
        }
    },

    /**
     * Reactivates the given menu item.
     * @protected
     * @param {JQuery} menuitem Menu item (`LI`) to reactivate.
     */
    reactivate: function(menuitem) {
        this.activeitem = menuitem;
        var submenu = menuitem.children('ul.ui-menu-child'),
        activeChilditem = submenu.children('li.ui-menuitem-active:first'),
        _self = this;

        if(activeChilditem.length == 1) {
            _self.deactivate(activeChilditem);
        }
    },

    /**
     * Highlights the given menu item by applying the proper CSS classes.
     * @param {JQuery} menuitem Menu item to highlight.
     */
    highlight: function(menuitem) {
        this.activeitem = menuitem;
        menuitem.children('a.ui-menuitem-link').addClass('ui-state-hover');
        menuitem.addClass('ui-menuitem-active');
    },

    /**
     * Shows the given submenu of a menu item.
     * @param {JQuery} menuitem A menu item (`LI`) with children.
     * @param {JQuery} submenu A child of the menu item.
     */
    showSubmenu: function(menuitem, submenu) {
        var pos ={
            my: 'left top',
            at: 'right top',
            of: menuitem,
            collision: 'flipfit'
        };

        submenu.css('z-index', PrimeFaces.nextZindex())
            .show()
            .position(pos);
    },

    /**
     * Deactivates all items and resets the state of this widget to its orignal state such that only the top-level menu
     * items are shown. 
     */
    reset: function() {
        var $this = this;
        this.active = false;

        this.jq.find('li.ui-menuitem-active').each(function() {
            $this.deactivate($(this), true);
        });
    }
    
});

/**
 * __PrimeFaces Menubar Widget__
 * 
 * Menubar is a horizontal navigation component.
 * 
 * @interface {PrimeFaces.widget.MenubarCfg} cfg The configuration for the {@link  Menubar| Menubar 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.TieredMenuCfg} cfg
 * 
 * @prop {number} cfg.delay Delay in milliseconds before displaying the submenu. Default is 0 meaning immediate.
 */
PrimeFaces.widget.Menubar = PrimeFaces.widget.TieredMenu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} menuitem
     * @param {JQuery} submenu
     */
    showSubmenu: function(menuitem, submenu) {
        var pos = null;

        if(menuitem.parent().hasClass('ui-menu-child')) {
            pos = {
                my: 'left top',
                at: 'right top',
                of: menuitem,
                collision: 'flipfit'
            };
        }
        else {
            pos = {
                my: 'left top',
                at: 'left bottom',
                of: menuitem,
                collision: 'flipfit'
            };
        }

        //avoid queuing multiple runs
        if(this.timeoutId) {
            clearTimeout(this.timeoutId);
        }

        //avoid using timeout if delay is 0
        if(this.cfg.delay && this.cfg.delay > 0) {
            this.timeoutId = setTimeout(function () {
               submenu.css('z-index', PrimeFaces.nextZindex())
                      .show()
                      .position(pos)
            }, this.cfg.delay);
        } else {
            submenu.css('z-index', PrimeFaces.nextZindex())
                   .show()
                   .position(pos);
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindKeyEvents: function() {
        var $this = this;

        this.keyboardTarget.on('focus.menubar', function(e) {
            $this.highlight($this.links.eq(0).parent());
        })
        .on('blur.menubar', function() {
            $this.reset();
        })
        .on('keydown.menu', function(e) {
            var currentitem = $this.activeitem;
            if(!currentitem) {
                return;
            }

            var isRootLink = !currentitem.closest('ul').hasClass('ui-menu-child'),
            keyCode = $.ui.keyCode;

            switch(e.which) {
                    case keyCode.LEFT:
                        if(isRootLink) {
                            var prevItem = currentitem.prevAll('.ui-menuitem:not(.ui-menubar-options):first');
                            if(prevItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(prevItem);
                            }

                            e.preventDefault();
                        }
                        else {
                            if(currentitem.hasClass('ui-menu-parent') && currentitem.children('.ui-menu-child').is(':visible')) {
                                $this.deactivate(currentitem);
                                $this.highlight(currentitem);
                            }
                            else {
                                var parentItem = currentitem.parent().parent();
                                $this.deactivate(currentitem);
                                $this.deactivate(parentItem);
                                $this.highlight(parentItem);
                            }
                        }
                    break;

                    case keyCode.RIGHT:
                        if(isRootLink) {
                            var nextItem = currentitem.nextAll('.ui-menuitem:not(.ui-menubar-options):first');
                            if(nextItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(nextItem);
                            }

                            e.preventDefault();
                        }
                        else {
                            if(currentitem.hasClass('ui-menu-parent')) {
                                var submenu = currentitem.children('.ui-menu-child');

                                if(submenu.is(':visible'))
                                    $this.highlight(submenu.children('.ui-menuitem:first'));
                                else
                                    $this.activate(currentitem);
                            }
                        }
                    break;

                    case keyCode.UP:
                        if(!isRootLink) {
                            var prevItem = currentitem.prev('.ui-menuitem');
                            if(prevItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(prevItem);
                            }
                        }

                        e.preventDefault();
                    break;

                    case keyCode.DOWN:
                        if(isRootLink) {
                            var submenu = currentitem.children('ul.ui-menu-child');
                            if(submenu.is(':visible'))
                                $this.highlight(submenu.children('.ui-menuitem:first'));
                            else
                                $this.activate(currentitem);
                        }
                        else {
                            var nextItem = currentitem.next('.ui-menuitem');
                            if(nextItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(nextItem);
                            }
                        }

                        e.preventDefault();
                    break;

                    case keyCode.ENTER:
                        var currentLink = currentitem.children('.ui-menuitem-link');
                        currentLink.trigger('click');
                        $this.jq.trigger("blur");
                        PrimeFaces.utils.openLink(e, currentLink);
                    break;

            }
        });
    }

});

/**
 * __PrimeFaces SlideMenu Widget__
 * 
 * SlideMenu is used to display nested submenus with sliding animation.
 * 
 * @prop {JQuery} backward The DOM element for the link to navigate back to the previous menu page.
 * @prop {JQuery} submenus The DOM elements for the sub menu items other that the root menu items.
 * @prop {JQuery} content The DOM element for the slide menu content.
 * @prop {number} jqWidth Width of the menu container in pixels.
 * @prop {JQuery} links The DOM elements for the the links to sub menus.
 * @prop {boolean} rendered Whether this menu was already rendered.
 * @prop {JQuery} rootList The DOM elements for the root menu entries.
 * @prop {JQuery[]} stack A stack with the menu items that were selected. Used to slide back to the previous menu page.
 * @prop {JQuery} wrapper The DOM element for the wrapper of the slide menu.
 * 
 * @interface {PrimeFaces.widget.SlideMenuCfg} cfg The configuration for the {@link  SlideMenu| SlideMenu 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.MenuCfg} cfg
 */
PrimeFaces.widget.SlideMenu = PrimeFaces.widget.Menu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        //elements
        this.submenus = this.jq.find('ul.ui-menu-list');
        this.wrapper = this.jq.children('div.ui-slidemenu-wrapper');
        this.content = this.wrapper.children('div.ui-slidemenu-content');
        this.rootList = this.content.children('ul.ui-menu-list');
        this.links = this.jq.find('a.ui-menuitem-link:not(.ui-state-disabled)');
        this.backward = this.wrapper.children('div.ui-slidemenu-backward');
        this.rendered = false;

        //config
        this.stack = [];
        this.jqWidth = this.jq.width();

        if(!this.jq.hasClass('ui-menu-dynamic')) {

            if(this.jq.is(':not(:visible)')) {
                var hiddenParent = this.jq.closest('.ui-hidden-container'),
                $this = this;

                if(hiddenParent.length) {
                    PrimeFaces.addDeferredRender(this.id, hiddenParent.attr('id'), function() {
                        return $this.render();
                    });
                }
            }
            else {
                this.render();
            }
        }

        this.bindEvents();
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.links.on("mouseenter", function() {
           $(this).addClass('ui-state-hover');
        })
        .on("mouseleave", function() {
           $(this).removeClass('ui-state-hover');
        })
        .on("click", function(e) {
            var link = $(this),
            submenu = link.next();

            if(submenu.length) {
               $this.forward(submenu);
               e.preventDefault();
            }
        });

        this.backward.on("click", function() {
            $this.back();
        });
    },

    /**
     * Slides to the given sub menu.
     * @param {JQuery} submenu A sub menu to show, with the class `ui-menuitem-link`.
     */
    forward: function(submenu) {
        var _self = this;

        this.push(submenu);

        var rootLeft = -1 * (this.depth() * this.jqWidth);

        submenu.show().css({
            left: this.jqWidth + 'px'
        });

        this.rootList.animate({
            left: rootLeft
        }, 500, 'easeInOutCirc', function() {
            if(_self.backward.is(':hidden')) {
                _self.backward.fadeIn('fast');
            }
        });
    },

    /**
     * Slides back to the previous menu page.
     */
    back: function() {
        if(!this.rootList.is(':animated')) {
            var _self = this,
            last = this.pop(),
            depth = this.depth();

            var rootLeft = -1 * (depth * this.jqWidth);

            this.rootList.animate({
                left: rootLeft
            }, 500, 'easeInOutCirc', function() {
                if(last) {
                    last.hide();
                }

                if(depth == 0) {
                    _self.backward.fadeOut('fast');
                }
            });
        }
    },

    /**
     * Adds the menu page to the top of the stack.
     * @param {JQuery} submenu A menu page to push to the stack. 
     * @private
     */
    push: function(submenu) {
        this.stack.push(submenu);
    },

    /**
     * Pops the most recently a menu page from the stack and return it.
     * @return {JQuery | null} The item on top of the stack, or `null` if the stack is empty.
     * @private
     */
    pop: function() {
        return this.stack.length !== 0 ? this.stack.pop() : null;
    },

    /**
     * Peeks the stack and returns the topmost item.
     * @return {JQuery | undefined} The last item on the stack, or `undefined` if the stack is empty
     * @private
     */
    last: function() {
        return this.stack[this.stack.length - 1];
    },

    /**
     * Inspects the stack and returns its size.
     * @return {number} The number of items on the stack.
     * @private
     */
    depth: function() {
        return this.stack.length;
    },

    /**
     * Renders the client-side parts of this widget.
     * @private
     */
    render: function() {
        this.submenus.width(this.jq.width());
        this.wrapper.height(this.rootList.outerHeight(true) + this.backward.outerHeight(true));
        this.content.height(this.rootList.outerHeight(true));
        this.rendered = true;
    },

    /**
     * @override
     * @inheritdoc
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    if (!$this.rendered) {
                        $this.render();
                    }
                    $this.jq.css('z-index', PrimeFaces.nextZindex());
                    $this.align();
                },
                onEntered: function() {
                    $this.bindPanelEvents();
                }
            });
        }
    }
});


/**
 * __PrimeFaces PlainMenu Widget__
 * 
 * Menu is a navigation component with sub menus and menu items.
 * 
 * @prop {JQuery} menuitemLinks DOM elements with the links of each menu item.
 * @prop {string} stateKey Name of the HTML5 Local Store that is used to store the state of this plain menu (expanded / collapsed
 * menu items).
 * @prop {string[]} collapsedIds A list with the ID of each menu item (with children) that is collapsed.
 * 
 * @interface {PrimeFaces.widget.PlainMenuCfg} cfg The configuration for the {@link  PlainMenu| PlainMenu 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.MenuCfg} cfg
 * 
 * @prop {boolean} cfg.toggleable `true` if grouped items can be toggled (expanded / collapsed), or `false` otherwise.
 */
PrimeFaces.widget.PlainMenu = PrimeFaces.widget.Menu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.menuitemLinks = this.jq.find('.ui-menuitem-link:not(.ui-state-disabled)');

        //events
        this.bindEvents();

        if(this.cfg.toggleable) {
            this.collapsedIds = [];
            this.stateKey = PrimeFaces.createStorageKey(this.id, 'PlainMenu');
            this.restoreState();
        }
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.menuitemLinks.on("mouseenter", function(e) {
            if($this.jq.is(':focus')) {
                $this.jq.trigger("blur");
            }

            $(this).addClass('ui-state-hover');
        })
        .on("mouseleave", function(e) {
            $(this).removeClass('ui-state-hover');
        });

        if(this.cfg.overlay) {
            this.menuitemLinks.on("click", function() {
                $this.hide();
            });

            this.trigger.on('keydown.ui-menu', function(e) {
                var keyCode = $.ui.keyCode;

                switch(e.which) {
                    case keyCode.DOWN:
                        $this.keyboardTarget.trigger('focus.menu');
                        e.preventDefault();
                    break;

                    case keyCode.TAB:
                        if($this.jq.is(':visible')) {
                            $this.hide();
                        }
                    break;
                }
            });
        }

        if(this.cfg.toggleable) {
            this.jq.find('> .ui-menu-list > .ui-widget-header').on('mouseover.menu', function() {
                $(this).addClass('ui-state-hover');
            })
            .on('mouseout.menu', function() {
                $(this).removeClass('ui-state-hover');
            })
            .on('click.menu', function(e) {
                var header = $(this);

                if(header.find('> h3 > .ui-icon').hasClass('ui-icon-triangle-1-s'))
                    $this.collapseSubmenu(header, true);
                else
                    $this.expandSubmenu(header, true);

                PrimeFaces.clearSelection();
                e.preventDefault();
            });
        }

        this.keyboardTarget.on('focus.menu', function() {
            $this.menuitemLinks.eq(0).addClass('ui-state-hover');
        })
        .on('blur.menu', function() {
            $this.menuitemLinks.filter('.ui-state-hover').removeClass('ui-state-hover');
        })
        .on('keydown.menu', function(e) {
            var currentLink = $this.menuitemLinks.filter('.ui-state-hover'),
            keyCode = $.ui.keyCode;

            switch(e.which) {
                    case keyCode.UP:
                        var prevItem = currentLink.parent().prevAll('.ui-menuitem:first');
                        if(prevItem.length) {
                            currentLink.removeClass('ui-state-hover');
                            prevItem.children('.ui-menuitem-link').addClass('ui-state-hover');
                        }

                        e.preventDefault();
                    break;

                    case keyCode.DOWN:
                        var nextItem = currentLink.parent().nextAll('.ui-menuitem:first');
                        if(nextItem.length) {
                            currentLink.removeClass('ui-state-hover');
                            nextItem.children('.ui-menuitem-link').addClass('ui-state-hover');
                        }

                        e.preventDefault();
                    break;

                    case keyCode.ENTER:
                        currentLink.trigger('click');
                        $this.jq.trigger("blur");
                        PrimeFaces.utils.openLink(e, currentLink);
                    break;

                    case keyCode.ESCAPE:
                        $this.hide();

                        if($this.cfg.overlay) {
                            $this.trigger.trigger('focus');
                        }
                    break;

            }
        });
    },

    /**
     * Collapses the given sub menu so that the children of that sub menu are not visible anymore.
     * @param {JQuery} header Menu item with children to collapse.
     * @param {boolean} [stateful] `true` if the new state of this menu (which items are collapsed and expanded) should
     * be saved (in an HTML5 Local Store), `false` otherwise. 
     */
    collapseSubmenu: function(header, stateful) {
        var items = header.nextUntil('li.ui-widget-header');

        header.attr('aria-expanded', false)
                .find('> h3 > .ui-icon').removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');

        items.filter('.ui-submenu-child').hide();

        if(stateful) {
            this.collapsedIds.push(header.attr('id'));
            this.saveState();
        }
    },

    /**
     * Expands the given sub menu so that the children of that sub menu become visible.
     * @param {JQuery} header Menu item with children to expand.
     * @param {boolean} [stateful] `true` if the new state of this menu (which items are collapsed and expanded) should
     * be saved (in an HTML5 Local Store), `false` otherwise. 
     */
    expandSubmenu: function(header, stateful) {
        var items = header.nextUntil('li.ui-widget-header');

        header.attr('aria-expanded', false)
                .find('> h3 > .ui-icon').removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-s');

        items.filter('.ui-submenu-child').show();

        if(stateful) {
            var id = header.attr('id');
            this.collapsedIds = $.grep(this.collapsedIds, function(value) {
                return (value !== id);
            });
            this.saveState();
        }
    },

    /**
     * Saves the current state (expanded / collapsed menu items) of this plain menu. Used to preserve the state during
     * AJAX updates as well as between page reloads. The state is stored in an HTML5 Local Store.
     * @private
     */
    saveState: function() {
        localStorage.setItem(this.stateKey, this.collapsedIds.join(','));
    },

    /**
     * Restores that state as stored by `saveState`. Usually called after an AJAX update and on page load.
     * @private
     */
    restoreState: function() {
        var collapsedIdsAsString = localStorage.getItem(this.stateKey);

        if(collapsedIdsAsString) {
            this.collapsedIds = collapsedIdsAsString.split(',');

            for(var i = 0 ; i < this.collapsedIds.length; i++) {
                this.collapseSubmenu($(PrimeFaces.escapeClientId(this.collapsedIds[i])), false);
            }
        }
    },

    /**
     * Clear the saved state (collapsed / expanded menu items) of this plain menu.
     * @private
     */
    clearState: function() {
        localStorage.removeItem(this.stateKey);
    }

});

/**
 * __PrimeFaces MenuButton Widget__
 *
 * MenuButton displays different commands in a popup menu.
 *
 * @prop {JQuery} button The DOM element for the menu button.
 * @prop {JQuery} menu The DOM element for the menu overlay panel.
 * @prop {JQuery} menuitems The DOM elements for the individual menu entries.
 * @prop {string} menuId Client ID of the menu overlay panel.
 *
 * @interface {PrimeFaces.widget.MenuButtonCfg} cfg The configuration for the {@link  MenuButton| MenuButton 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.TieredMenuCfg} cfg
 *
 * @prop {boolean} cfg.disabled Whether this menu button is initially disabled.
 * @prop {string} cfg.collision When the positioned element overflows the window in some direction, move it to an
 * alternative position. Similar to my and at, this accepts a single value or a pair for horizontal/vertical,
 * e.g., `flip`, `fit`, `fit flip`, `fit none`.
 */
PrimeFaces.widget.MenuButton = PrimeFaces.widget.TieredMenu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.menuId = this.jqId + '_menu';
        this.button = this.jq.children('button');
        this.menu = this.jq.children('.ui-menu');
        this.menuitems = this.jq.find('.ui-menuitem');
        this.cfg.disabled = this.button.is(':disabled');

        if(!this.cfg.disabled) {
            this.bindButtonEvents();
            PrimeFaces.utils.registerDynamicOverlay(this, this.menu, this.id + '_menu');
            this.transition = PrimeFaces.utils.registerCSSTransition(this.menu, 'ui-connected-overlay');
        }
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} menuitem
     * @param {JQuery} submenu
     */
    showSubmenu: function(menuitem, submenu) {
        var pos = {
            my: 'left top',
            at: 'right top',
            of: menuitem,
            collision: 'flipfit'
        };

        //avoid queuing multiple runs
        if(this.timeoutId) {
            clearTimeout(this.timeoutId);
        }

        this.timeoutId = setTimeout(function () {
           submenu.css('z-index', PrimeFaces.nextZindex())
                  .show()
                  .position(pos);
        }, this.cfg.delay);
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindButtonEvents: function() {
        var $this = this;

        //button visuals
        this.button.on("mouseover", function(){
            if(!$this.button.hasClass('ui-state-focus')) {
                $this.button.addClass('ui-state-hover');
            }
        }).on("mouseout", function() {
            if(!$this.button.hasClass('ui-state-focus')) {
                $this.button.removeClass('ui-state-hover ui-state-active');
            }
        }).on("mousedown", function() {
            $(this).removeClass('ui-state-focus ui-state-hover').addClass('ui-state-active');
        }).on("mouseup", function() {
            var el = $(this);
            el.removeClass('ui-state-active');

            if($this.menu.is(':visible')) {
                el.addClass('ui-state-hover');
                $this.hide();
            }
            else {
                el.addClass('ui-state-focus');
                $this.show();
            }
        }).on("focus", function() {
            $(this).addClass('ui-state-focus');
        }).on("blur", function() {
            $(this).removeClass('ui-state-focus');
        });

        //mark button and descandants of button as a trigger for a primefaces overlay
        this.button.data('primefaces-overlay-target', true).find('*').data('primefaces-overlay-target', true);

        //menuitem visuals
        this.menuitems.on("mouseover", function(e) {
            var element = $(this);
            if(!element.hasClass('ui-state-disabled')) {
                element.addClass('ui-state-hover');
            }
        }).on("mouseout", function(e) {
            $(this).removeClass('ui-state-hover');
        }).on("click", function() {
            $this.button.removeClass('ui-state-focus');
            $this.hide();
        });

        //keyboard support
        this.button.on("keydown", function(e) {
            var keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.UP:
                    if($this.menu.is(':visible')) {
                        var highlightedItem = $this.menuitems.filter('.ui-state-hover'),
                        prevItems = highlightedItem.length ? highlightedItem.prevAll(':not(.ui-separator)') : null;

                        if(prevItems && prevItems.length) {
                            highlightedItem.removeClass('ui-state-hover');
                            prevItems.eq(0).addClass('ui-state-hover');
                        }
                    }
                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    if($this.menu.is(':visible')) {
                        var highlightedItem = $this.menuitems.filter('.ui-state-hover'),
                        nextItems = highlightedItem.length ? highlightedItem.nextAll(':not(.ui-separator)') : $this.menuitems.eq(0);

                        if(nextItems.length) {
                            highlightedItem.removeClass('ui-state-hover');
                            nextItems.eq(0).addClass('ui-state-hover');
                        }
                    }
                    e.preventDefault();
                break;

                case keyCode.ENTER:
                case keyCode.SPACE:
                    if($this.menu.is(':visible'))
                        $this.menuitems.filter('.ui-state-hover').children('a').trigger('click');
                    else
                        $this.show();

                    e.preventDefault();
                break;


                case keyCode.ESCAPE:
                case keyCode.TAB:
                    $this.hide();
                break;
            }
        });

        //aria
        this.button.attr('role', 'button').attr('aria-disabled', this.button.is(':disabled'));
    },

    /**
     * Sets up all panel event listeners
     *
     * @override
     */
    bindPanelEvents: function() {
        var $this = this;

        if (!$this.cfg.disabled) {
            this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.menu,
                function() { return $this.button; },
                function(e, eventTarget) {
                    if (!($this.menu.is(eventTarget) || $this.menu.has(eventTarget).length > 0)) {
                        $this.button.removeClass('ui-state-focus ui-state-hover');
                        $this.hide();
                    }
                });
        }

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', this.menu, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     *
     * @override
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Brings up the overlay menu with the menu items, as if the menu button were pressed.
     *
     * @override
     */
    show: function() {
        var $this = this;

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    $this.menu.css('z-index', PrimeFaces.nextZindex());
                    $this.alignPanel();
                },
                onEntered: function() {
                    $this.bindPanelEvents();
                }
            });
        }
    },

    /**
     * Hides the overlay menu with the menu items, as if the user clicked outside the menu.
     *
     * @override
     */
    hide: function() {
        if (this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    $this.menuitems.filter('.ui-state-hover').removeClass('ui-state-hover');
                }
            });
        }
    },

    /**
     * Align the overlay panel with the menu items so that it is positioned next to the menu button.
     */
    alignPanel: function() {
        this.menu.css({left:'', top:'', 'transform-origin': 'center top'});

        if(this.menu.parent().is(this.jq)) {
            this.menu.css({
                left: '0px',
                top: this.jq.innerHeight() + 'px'
            });
        }
        else {
            this.menu.position({
                my: 'left top',
                at: 'left bottom',
                of: this.button,
                collision: this.cfg.collision || "flip",
                using: function(pos, directions) {
                    $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                }
            });
        }
    }

});

/**
 * __PrimeFaces ContextMenu Widget__
 * 
 * ContextMenu provides an overlay menu displayed on mouse right-click event.
 * 
 * @typedef {"single" | "multiple"} PrimeFaces.widget.ContextMenu.SelectionMode  Selection mode for the context, whether
 * the user may select only one or multiple items at the same time.
 * 
 * @typedef PrimeFaces.widget.ContextMenu.BeforeShowCallback Client side callback invoked before the context menu is
 * shown.
 * @this {PrimeFaces.widget.ContextMenu} PrimeFaces.widget.ContextMenu.BeforeShowCallback
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.ContextMenu.BeforeShowCallback.event Event that triggered the context menu to
 * be shown (e.g. a mouse click).
 * @return {boolean} PrimeFaces.widget.ContextMenu.BeforeShowCallback ` true` to show the context menu, `false` to
 * prevent is from getting displayed.
 * 
 * @interface {PrimeFaces.widget.ContextMenu.ContextMenuProvider} ContextMenuProvider Interface for widgets that wish to
 * provide a context menu. They need to implement the `bindContextMenu` method.  This method is called once when the
 * context menu is initialized. Widgets should register the appropriate event listeners and call `menuWidget.show()`
 * to bring up the context menu.
 * @template ContextMenuProvider.TTarget Type of the widget that wishes to provide a context menu.
 * @method ContextMenuProvider.bindContextMenu Callback that is invoked when the context menu is initialized. Lets the
 * context menu provider register the appropriate event listeners for when the context menu should be shown and hidden.
 * @param {PrimeFaces.widget.ContextMenu} ContextMenuProvider.bindContextMenu.menuWidget The widget instance of the
 * context menu.
 * @param {TTarget} ContextMenuProvider.bindContextMenu.targetWidget The widget instance of the target widget that wants
 * to add a context menu.
 * @param {string | JQuery} ContextMenuProvider.bindContextMenu.targetId ID selector or DOM element of the target, i.e.
 * the element the context menu belongs to.
 * @param {PrimeFaces.widget.ContextMenuCfg} ContextMenuProvider.bindContextMenu.cfg The current configuration of the
 * context menu.
 * 
 * @prop {JQuery} jqTarget Target element of this context menu. A right click on the target brings up this context menu.
 * @prop {string | JQuery} jqTargetId ID selector or DOM element of the target, i.e. the element this context menu
 * belongs to.
 * 
 * @interface {PrimeFaces.widget.ContextMenuCfg} cfg The configuration for the {@link  ContextMenu| ContextMenu 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.TieredMenuCfg} cfg
 * 
 * @prop {string} cfg.appendTo Search expression for the element to which this context menu is appended. This is usually
 * invoke before the context menu is shown. When it returns `false`, this context menu is not shown.
 * @prop {PrimeFaces.widget.ContextMenu.BeforeShowCallback} cfg.beforeShow Client side callback invoked before the
 * context menu is shown.
 * @prop {string} cfg.event Event that triggers this context menu, usually a (right) mouse click.
 * @prop {PrimeFaces.widget.ContextMenu.SelectionMode} cfg.selectionMode Defines the selection behavior.
 * @prop {string} cfg.target Client ID of the target widget.
 * @prop {string} cfg.targetFilter Selector to filter the elements to attach the menu.
 * @prop {string} cfg.targetWidgetVar Widget variable of the target widget.
 */
PrimeFaces.widget.ContextMenu = PrimeFaces.widget.TieredMenu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        cfg.autoDisplay = true;
        this._super(cfg);
        this.cfg.selectionMode = this.cfg.selectionMode||'multiple';

        var $this = this,
        documentTarget = (this.cfg.target === undefined);

        //event
        this.cfg.event = this.cfg.event||'contextmenu';

        //target
        this.jqTargetId = documentTarget ? document : PrimeFaces.escapeClientId(this.cfg.target);
        this.jqTarget = $(this.jqTargetId);

        //append to body
        this.cfg.appendTo = '@(body)';
        PrimeFaces.utils.registerDynamicOverlay(this, this.jq, this.id);

        //attach contextmenu
        if(documentTarget) {
            var event = 'contextmenu.' + this.id + '_contextmenu';
            
            $(document).off(event).on(event, function(e) {
                $this.show(e);
            });

            if (PrimeFaces.env.isTouchable(this.cfg)) {
                $(document).swipe({
                    longTap:function(e, target) {
                       $this.show(e);
                    }
                });
            }
        }
        else {
            var binded = false;

            if (this.cfg.targetWidgetVar) {
                var targetWidget = PrimeFaces.widgets[this.cfg.targetWidgetVar];

                if (targetWidget) {
                    if (typeof targetWidget.bindContextMenu === 'function') {
                        targetWidget.bindContextMenu(this, targetWidget, this.jqTargetId, this.cfg);
                        // GitHub #6776 IOS needs long touch on table/tree but Android does not
                        if(PrimeFaces.env.ios) {
                            $this.bindTouchEvents();
                        }
                        binded = true;
                    }
                }
                else {
                    PrimeFaces.warn("ContextMenu targets a widget which is not available yet. Please place the contextMenu after the target component. targetWidgetVar: " + this.cfg.targetWidgetVar);
                }
            }

            if (binded === false) {
                var event = this.cfg.event + '.' + this.id + '_contextmenu';

                $(document).off(event, this.jqTargetId).on(event, this.jqTargetId, null, function(e) {
                    $this.show(e);
                });

                $this.bindTouchEvents();
            }
        }

        this.transition = PrimeFaces.utils.registerCSSTransition(this.jq, 'ui-connected-overlay');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindPanelEvents: function() {
        var $this = this;

        this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'click.' + this.id + '_hide', this.jq,
            function(e) { return e.which == 3 ? $this.jqTarget : null; },
            function(e, eventTarget) {
                if(!($this.jq.is(eventTarget) || $this.jq.has(eventTarget).length > 0)) {
                    $this.hide();
                }
            });

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.jqTarget, function() {
            $this.hide();
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Binds mobile touch events.
     * @protected
     */
    bindTouchEvents: function() {
        if (PrimeFaces.env.isTouchable(this.cfg)) {
             var $this = this;

             // GitHub #6776 turn off Copy/Paste menu for IOS
             if(PrimeFaces.env.ios) {
                $(document.body).addClass('ui-touch-selection-disabled');
             }

             $this.jqTarget.swipe({
                 longTap:function(e, target) {
                      $this.show(e);
                 }
             });
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindItemEvents: function() {
        this._super();

        var $this = this;

        //hide menu on item click
        this.links.on('click', function(e) {
            var target = $(e.target),
                submenuLink = target.hasClass('ui-submenu-link') ? target : target.closest('.ui-submenu-link');

            if (submenuLink.length) {
                return;
            }

            $this.hide();
        });
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery.TriggeredEvent} [e] The event that triggered this context menu to be shown.
     * 
     * Note:  __This parameter is not optional__, but is marked as such since this method overrides a parent method
     * that does not have any parameters. Do not (implicitly) cast an instance of this class to a parent type.
     */
    show: function(e) {
        var $this = this;

        if(this.cfg.targetFilter && $(e.target).is(':not(' + this.cfg.targetFilter + ')')) {
            return;
        }

        //hide other contextmenus if any
        $(document.body).children('.ui-contextmenu:visible').hide();

        if(this.cfg.beforeShow) {
            var retVal = this.cfg.beforeShow.call(this, e);
            if(retVal === false) {
                return;
            }
        }

        if (this.transition) {
            this.transition.show({
                onEnter: function() {
                    var win = $(window),
                    left = e.pageX,
                    top = e.pageY,
                    width = $this.jq.outerWidth(),
                    height = $this.jq.outerHeight();

                    //collision detection for window boundaries
                    if ((left + width) > (win.width())+ win.scrollLeft()) {
                        left = left - width;
                    }
                    if ((top + height ) > (win.height() + win.scrollTop())) {
                        top = top - height;
                    }
                    if (top < 0) {
                        top = e.pageY;
                    }

                    $this.jq.css({
                        'left': left + 'px',
                        'top': top + 'px',
                        'z-index': PrimeFaces.nextZindex(),
                        'transform-origin': 'center top'
                    });
                },
                onEntered: function() {
                    $this.bindPanelEvents();
                }
            });
        }

        e.preventDefault();
        e.stopPropagation();
    },

    /**
     * @override
     * @inheritdoc
     */
    hide: function() {
        if (this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    //hide submenus
                    $this.jq.find('li.ui-menuitem-active').each(function() {
                        $this.deactivate($(this), true);
                    });
                }
            });
        }
    },

    /**
     * Checks whether this context menu is open.
     * @return {boolean} `true` if this context menu is currently visible, `false` otherwise.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },

    /**
     * Finds the target element of this context menu. A right-click on that target element brings up this context menu. 
     * @private
     * @return {JQuery} The target element of this context men.
     */
    getTarget: function() {
        return this.jqTarget;
    }

});

/**
 * __PrimeFaces MegaMenu Widget__
 * 
 * MegaMenu is a horizontal navigation component that displays submenus together.
 * 
 * @prop {boolean} active Whether the current menu is active and displayed.
 * @prop {JQuery} activeitem The currently active (highlighted) menu item.
 * @prop {JQuery} keyboardTarget The DOM element for the input element accessible via keyboard keys.
 * @prop {JQuery} rootLinks The DOM elements for the root level menu links with the class `.ui-menuitem-link`. 
 * @prop {JQuery} rootList The DOM elements for the root level menu items with the class `.ui-menu-list`.
 * @prop {JQuery} subLinks The DOM elements for all menu links not a the root level, with the class `.ui-menuitem-link`.
 * 
 * @interface {PrimeFaces.widget.MegaMenuCfg} cfg The configuration for the {@link  MegaMenu| MegaMenu 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 {number} cfg.activeIndex Index of the menu item initially active.
 * @prop {boolean} cfg.autoDisplay Defines whether submenus will be displayed on mouseover or not. When set to false,
 * click event is required to display.
 * @prop {number} cfg.delay Delay in milliseconds before displaying the submenu. Default is 0 meaning immediate.
 * @prop {boolean} cfg.vertical `true` if the mega menu is displayed with a vertical layout, `false` if displayed with a
 * horizontal layout.
 */
PrimeFaces.widget.MegaMenu = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.vertical = this.jq.hasClass('ui-megamenu-vertical');
        this.rootList = this.jq.children('ul.ui-menu-list');
        this.rootLinks = this.rootList.find('> li.ui-menuitem > a.ui-menuitem-link:not(.ui-state-disabled)');
        this.subLinks = this.jq.find('.ui-menu-child a.ui-menuitem-link:not(.ui-state-disabled)');
        this.keyboardTarget = this.jq.children('.ui-helper-hidden-accessible');

        if(this.cfg.activeIndex !== undefined) {
            this.rootLinks.eq(this.cfg.activeIndex).addClass('ui-state-hover').closest('li.ui-menuitem').addClass('ui-menuitem-active');
        }

        this.bindEvents();
        this.bindKeyEvents();
    },


    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.rootLinks.on("mouseenter", function(e) {
            var link = $(this),
            menuitem = link.parent();

            var current = menuitem.siblings('.ui-menuitem-active');
            if(current.length > 0) {
                current.find('li.ui-menuitem-active').each(function() {
                    $this.deactivate($(this));
                });
                $this.deactivate(current, false);
            }

            if($this.cfg.autoDisplay||$this.active) {
                $this.activate(menuitem);
            }
            else {
                $this.highlight(menuitem);
            }

        });

        if(this.cfg.autoDisplay === false) {
            this.rootLinks.data('primefaces-megamenu', this.id).find('*').data('primefaces-megamenu', this.id)

            this.rootLinks.on("click", function(e) {
                var link = $(this),
                menuitem = link.parent(),
                submenu = link.next();

                if(submenu.length === 1) {
                    if(submenu.is(':visible')) {
                        $this.active = false;
                        $this.deactivate(menuitem, true);
                    }
                    else {
                        $this.active = true;
                        $this.activate(menuitem);
                    }
                }
                else {
                    PrimeFaces.utils.openLink(e, link);
                }

                e.preventDefault();
            });
        }
        else {
            this.rootLinks.filter('.ui-submenu-link').on("click", function(e) {
                e.preventDefault();
            });
        }

        this.subLinks.on("mouseenter", function() {
            if($this.activeitem && !$this.isRootLink($this.activeitem)) {
                $this.deactivate($this.activeitem);
            }
            $this.highlight($(this).parent());
        })
        .on("mouseleave", function() {
            if($this.activeitem && !$this.isRootLink($this.activeitem)) {
                $this.deactivate($this.activeitem);
            }
            $(this).removeClass('ui-state-hover');
        });

        this.rootList.on("mouseleave", function(e) {
            var activeitem = $this.rootList.children('.ui-menuitem-active');
            if(activeitem.length === 1) {
                $this.deactivate(activeitem, false);
            }
        });

        this.rootList.find('> li.ui-menuitem > ul.ui-menu-child').on("mouseleave", function(e) {
            e.stopPropagation();
        });

        $(document.body).on("click", function(e) {
            var target = $(e.target);
            if(target.data('primefaces-megamenu') === $this.id) {
                return;
            }

            $this.active = false;
            $this.deactivate($this.rootList.children('li.ui-menuitem-active'), true);
        });
    },

    /**
     * Sets up all keyboard-related event listeners.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        this.keyboardTarget.on('focus.megamenu', function(e) {
            $this.highlight($this.rootLinks.eq(0).parent());
        })
        .on('blur.megamenu', function() {
            $this.reset();
        })
        .on('keydown.megamenu', function(e) {
            var currentitem = $this.activeitem;
            if(!currentitem) {
                return;
            }

            var isRootLink = $this.isRootLink(currentitem),
            keyCode = $.ui.keyCode;

            switch(e.which) {
                    case keyCode.LEFT:
                        if(isRootLink && !$this.cfg.vertical) {
                            var prevItem = currentitem.prevAll('.ui-menuitem:first');
                            if(prevItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(prevItem);
                            }

                            e.preventDefault();
                        }
                        else {
                            if(currentitem.hasClass('ui-menu-parent') && currentitem.children('.ui-menu-child').is(':visible')) {
                                $this.deactivate(currentitem);
                                $this.highlight(currentitem);
                            }
                            else {
                                var parentItem = currentitem.closest('ul.ui-menu-child').parent();
                                if(parentItem.length) {
                                    $this.deactivate(currentitem);
                                    $this.deactivate(parentItem);
                                    $this.highlight(parentItem);
                                }
                            }
                        }
                    break;

                    case keyCode.RIGHT:
                        if(isRootLink && !$this.cfg.vertical) {
                            var nextItem = currentitem.nextAll('.ui-menuitem:visible:first');
                            if(nextItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(nextItem);
                            }

                            e.preventDefault();
                        }
                        else {

                            if(currentitem.hasClass('ui-menu-parent')) {
                                var submenu = currentitem.children('.ui-menu-child');
                                if(submenu.is(':visible')) {
                                    $this.highlight(submenu.find('ul.ui-menu-list:visible > .ui-menuitem:visible:first'));
                                }
                                else {
                                    $this.activate(currentitem);
                                }
                            }
                        }
                    break;

                    case keyCode.UP:
                        if(!isRootLink || $this.cfg.vertical) {
                            var prevItem = $this.findPrevItem(currentitem);
                            if(prevItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(prevItem);
                            }
                        }

                        e.preventDefault();
                    break;

                    case keyCode.DOWN:
                        if(isRootLink && !$this.cfg.vertical) {
                            var submenu = currentitem.children('ul.ui-menu-child');
                            if(submenu.is(':visible')) {
                                var firstMenulist = $this.getFirstMenuList(submenu);
                                $this.highlight(firstMenulist.children('.ui-menuitem:visible:first'));
                            }
                            else {
                                $this.activate(currentitem);
                            }
                        }
                        else {
                            var nextItem = $this.findNextItem(currentitem);
                            if(nextItem.length) {
                                $this.deactivate(currentitem);
                                $this.highlight(nextItem);
                            }
                        }

                        e.preventDefault();
                    break;

                    case keyCode.ENTER:
                        var currentLink = currentitem.children('.ui-menuitem-link');
                        currentLink.trigger('click');
                        $this.jq.trigger("blur");
                        var href = currentLink.attr('href');
                        if(href && href !== '#') {
                            window.location.href = href;
                        }
                        $this.deactivate(currentitem);
                        e.preventDefault();
                    break;

                    case keyCode.ESCAPE:
                        if(currentitem.hasClass('ui-menu-parent')) {
                            var submenu = currentitem.children('ul.ui-menu-list:visible');
                            if(submenu.length > 0) {
                                submenu.hide();
                            }
                        }
                        else {
                            var parentItem = currentitem.closest('ul.ui-menu-child').parent();
                            if(parentItem.length) {
                                $this.deactivate(currentitem);
                                $this.deactivate(parentItem);
                                $this.highlight(parentItem);
                            }
                        }
                        e.preventDefault();
                    break;
            }
        });
    },

    /**
     * Finds the menu items that preceeded the given item.
     * @param {JQuery} menuitem One of the menu items of this mega menu, with the class `.ui-menuitem`.
     * @return {JQuery} The menu item before the given item. Empty JQuery instance if the given item is the first.
     */
    findPrevItem: function(menuitem) {
        var previtem = menuitem.prev('.ui-menuitem');

        if(!previtem.length) {
            var prevSubmenu = menuitem.closest('ul.ui-menu-list').prev('.ui-menu-list');

            if(!prevSubmenu.length) {
                prevSubmenu = menuitem.closest('td').prev('td').children('.ui-menu-list:visible:last');
            }

            if(prevSubmenu.length) {
                previtem = prevSubmenu.find('li.ui-menuitem:visible:last');
            }
        }
        return previtem;
    },

    /**
     * Finds the menu items that succeeds the given item.
     * @param {JQuery} menuitem One of the menu items of this mega menu, with the class `.ui-menuitem`.
     * @return {JQuery} The menu item after the given item. Empty JQuery instance if the given item is the last.
     */
    findNextItem: function(menuitem) {
        var nextitem = menuitem.next('.ui-menuitem');

        if(!nextitem.length) {
            var nextSubmenu = menuitem.closest('ul.ui-menu-list').next('.ui-menu-list');
            if(!nextSubmenu.length) {
                nextSubmenu = menuitem.closest('td').next('td').children('.ui-menu-list:visible:first');
            }

            if(nextSubmenu.length) {
                nextitem = nextSubmenu.find('li.ui-menuitem:visible:first');
            }
        }
        return nextitem;
    },

    /**
     * Finds the the menu group of the given submenu, i.e. the children of the given item.
     * @param {JQuery} submenu A submenu with children.
     * @return {JQuery} The first sub menu list, an item with the class `.ui-menu-list`.
     */
    getFirstMenuList: function(submenu) {
        return submenu.find('.ui-menu-list:not(.ui-state-disabled):first');
    },

    /**
     * Checks whether the given menu item is the root menu item element.
     * @param {JQuery} menuitem One of the menu items of this mega menu.
     * @return {boolean} `true` if the given menu item is the root, or `false` otherwise.
     */
    isRootLink: function(menuitem) {
        var submenu = menuitem.closest('ul');
        return submenu.parent().hasClass('ui-menu');
    },

    /**
     * Resets the entire mega menu, i.e. closes all opened sub menus.
     */
    reset: function() {
        var $this = this;
        this.active = false;

        this.jq.find('li.ui-menuitem-active').each(function() {
            $this.deactivate($(this), true);
        });
    },

    /**
     * Deactivates the menu item, i.e. closes the sub menu.
     * @param {JQuery} menuitem A menu item to close.
     * @param {boolean} [animate] If `true`, closes the sub menu with an animation, or `false` otherwise. 
     */
    deactivate: function(menuitem, animate) {
        var link = menuitem.children('a.ui-menuitem-link'),
        submenu = link.next();

        menuitem.removeClass('ui-menuitem-active');
        link.removeClass('ui-state-hover');
        this.activeitem = null;

        if(submenu.length > 0) {
            if(animate)
                submenu.fadeOut('fast');
            else
                submenu.hide();
        }
    },

    /**
     * Highlight the given menu entry, as if the user were to hover it.
     * @param {JQuery} menuitem A menu entry to highlight.
     */
    highlight: function(menuitem) {
        var link = menuitem.children('a.ui-menuitem-link');

        menuitem.addClass('ui-menuitem-active');
        link.addClass('ui-state-hover');
        this.activeitem = menuitem;
    },

    /**
     * Activates the menu item, i.e. opens the sub menu.
     * @param {JQuery} menuitem A menu item to open.
     */
    activate: function(menuitem) {
        var submenu = menuitem.children('.ui-menu-child'),
        $this = this;

        $this.highlight(menuitem);

        if(submenu.length > 0) {
            $this.showSubmenu(menuitem, submenu);
        }
    },

    /**
     * Opens and shows the sub menu of the given menu item.
     * @param {JQuery} menuitem A menu item with a submenu. 
     * @param {JQuery} submenu One of the submenus of the given menu item to show. 
     * @private
     */
    showSubmenu: function(menuitem, submenu) {
        var pos = null;

        if(this.cfg.vertical) {
            pos = {
                my: 'left top',
                at: 'right top',
                of: menuitem,
                collision: 'flipfit'
            };
        }
        else {
            pos = {
                my: 'left top',
                at: 'left bottom',
                of: menuitem,
                collision: 'flipfit'
            };
        }

        //avoid queuing multiple runs
        if(this.timeoutId) {
            clearTimeout(this.timeoutId);
        }

        this.timeoutId = setTimeout(function () {
           submenu.css('z-index', PrimeFaces.nextZindex())
                  .show()
                  .position(pos)
        }, this.cfg.delay);
    }

});

/**
 * __PrimeFaces PanelMenu Widget__
 * 
 * PanelMenu is a hybrid component of accordionPanel and tree components.
 * 
 * @prop {string[]} expandedNodes A list of IDs of the menu items that are currently expanded.
 * @prop {boolean} focusCheck Flag for IE to keep track of whether an item was focused.
 * @prop {JQuery | null} focusedItem The DOM elements for the menu item that is currently focused.
 * @prop {JQuery} headers The DOM elements for the accordion panel headers that can be expanded and collapsed.
 * @prop {JQuery} menuitemLinks The DOM elements for the menu items inside each accordion panel that can be clicked.
 * @prop {JQuery} menuContent The DOM elements for the content container of each accordion panel.
 * @prop {JQuery} menuText The DOM elements for the text of each menu entry in the accordion panels.
 * @prop {string} stateKey Key used to store the UI state (expanded items) in an HTML5 Local Store. 
 * @prop {JQuery} treeLinks  The DOM elements for the clickable links with a sub menu that is shown upon clicking the
 * link. 
 * 
 * @interface {PrimeFaces.widget.PanelMenuCfg} cfg The configuration for the {@link  PanelMenu| PanelMenu 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.multiple Whether multiple accordion menu items are allowed to be expanded at the same time.
 * @prop {boolean} cfg.stateful Whether the UI state (expanded menu items) should be persisted in an HTML5 Local Store.
 */
PrimeFaces.widget.PanelMenu = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.headers = this.jq.find('> .ui-panelmenu-panel > h3.ui-panelmenu-header:not(.ui-state-disabled)');
        this.menuContent = this.jq.find('> .ui-panelmenu-panel > .ui-panelmenu-content');
        this.menuitemLinks = this.menuContent.find('.ui-menuitem-link:not(.ui-state-disabled)');
        this.menuText = this.menuitemLinks.find('.ui-menuitem-text');
        this.treeLinks = this.menuContent.find('.ui-menu-parent > .ui-menuitem-link:not(.ui-state-disabled)');

        //keyboard support
        this.focusedItem = null;
        this.menuText.attr('tabindex', -1);

        //ScreenReader support
        this.menuText.attr('role', 'menuitem');
        this.treeLinks.find('> .ui-menuitem-text').attr('aria-expanded', false);

        this.bindEvents();

        if(this.cfg.stateful) {
            this.stateKey = PrimeFaces.createStorageKey(this.id, 'PanelMenu');
        }

        this.restoreState();
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.headers.on("mouseover", function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')) {
                element.addClass('ui-state-hover');
            }
        }).on("mouseout", function() {
            var element = $(this);
            if(!element.hasClass('ui-state-active')) {
                element.removeClass('ui-state-hover');
            }
        }).on("click", function(e) {
            var header = $(this);

            if (!$this.cfg.multiple) {
                $this.collapseActiveSibling(header);
            }

            if (header.hasClass('ui-state-active'))
                $this.collapseRootSubmenu($(this));
            else
                $this.expandRootSubmenu($(this), false);

            $this.removeFocusedItem();
            header.trigger('focus');
            e.preventDefault();
        });

        this.menuitemLinks.on("mouseover", function() {
            $(this).addClass('ui-state-hover');
        }).on("mouseout", function() {
            $(this).removeClass('ui-state-hover');
        }).on("click", function(e) {
            var currentLink = $(this);
            $this.focusItem(currentLink.closest('.ui-menuitem'));
            PrimeFaces.utils.openLink(e, currentLink);
        });

        this.treeLinks.on("click", function(e) {
            var link = $(this),
            submenu = link.parent(),
            submenuList = link.next();

            if(submenuList.is(':visible'))
                $this.collapseTreeItem(submenu);
            else
                $this.expandTreeItem(submenu, false);

            e.preventDefault();
        });

        this.bindKeyEvents();
    },

    /**
     * Sets up the keyboard event listeners required by this panel menu widget.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this;

        if(PrimeFaces.env.isIE()) {
            this.focusCheck = false;
        }

        this.headers.on('focus.panelmenu', function(){
            $(this).addClass('ui-menuitem-outline');
        })
        .on('blur.panelmenu', function(){
            $(this).removeClass('ui-menuitem-outline ui-state-hover');
        })
        .on('keydown.panelmenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                $(this).trigger('click');
                e.preventDefault();
            }
        });

        this.menuContent.on('mousedown.panelmenu', function(e) {
            if($(e.target).is(':not(:input:enabled)')) {
                e.preventDefault();
            }
        }).on('focus.panelmenu', function(){
            if(!$this.focusedItem) {
                $this.focusItem($this.getFirstItemOfContent($(this)));
                if(PrimeFaces.env.isIE()) {
                    $this.focusCheck = false;
                }
            }
        });

        this.menuContent.off('keydown.panelmenu blur.panelmenu').on('keydown.panelmenu', function(e) {
            if(!$this.focusedItem) {
                return;
            }

            var keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.LEFT:
                    if($this.isExpanded($this.focusedItem)) {
                        $this.focusedItem.children('.ui-menuitem-link').trigger('click');
                    }
                    else {
                        var parentListOfItem = $this.focusedItem.closest('ul.ui-menu-list');

                        if(parentListOfItem.parent().is(':not(.ui-panelmenu-content)')) {
                            $this.focusItem(parentListOfItem.closest('li.ui-menuitem'));
                        }
                    }

                    e.preventDefault();
                break;

                case keyCode.RIGHT:
                    if($this.focusedItem.hasClass('ui-menu-parent') && !$this.isExpanded($this.focusedItem)) {
                        $this.focusedItem.children('.ui-menuitem-link').trigger('click');
                    }
                    e.preventDefault();
                break;

                case keyCode.UP:
                    var itemToFocus = null,
                    prevItem = $this.focusedItem.prev();

                    if(prevItem.length) {
                        itemToFocus = prevItem.find('li.ui-menuitem:visible:last');
                        if(!itemToFocus.length) {
                            itemToFocus = prevItem;
                        }
                    }
                    else {
                        itemToFocus = $this.focusedItem.closest('ul').parent('li');
                    }

                    if(itemToFocus.length) {
                        $this.focusItem(itemToFocus);
                    }

                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    var itemToFocus = null,
                    firstVisibleChildItem = $this.focusedItem.find('> ul > li:visible:first');

                    if(firstVisibleChildItem.length) {
                        itemToFocus = firstVisibleChildItem;
                    }
                    else if($this.focusedItem.next().length) {
                        itemToFocus = $this.focusedItem.next();
                    }
                    else {
                        if($this.focusedItem.next().length === 0) {
                            itemToFocus = $this.searchDown($this.focusedItem);
                        }
                    }

                    if(itemToFocus && itemToFocus.length) {
                        $this.focusItem(itemToFocus);
                    }

                    e.preventDefault();
                break;

                case keyCode.ENTER:
                case keyCode.SPACE:
                    var currentLink = $this.focusedItem.children('.ui-menuitem-link');
                    //IE fix
                    setTimeout(function(){
                        currentLink.trigger('click');
                    },1);
                    $this.jq.trigger("blur");

                    var href = currentLink.attr('href');
                    if(href && href !== '#') {
                        window.location.href = href;
                    }
                    e.preventDefault();
                break;

                case keyCode.TAB:
                    if($this.focusedItem) {
                        if(PrimeFaces.env.isIE()) {
                            $this.focusCheck = true;
                        }
                        $(this).trigger('focus');
                    }
                break;
            }
        }).on('blur.panelmenu', function(e) {
            if(PrimeFaces.env.isIE() && !$this.focusCheck) {
                return;
            }

            $this.removeFocusedItem();
        });

        var clickNS = 'click.' + this.id;
        //remove focusedItem when document is clicked
        $(document.body).off(clickNS).on(clickNS, function(event) {
            if(!$(event.target).closest('.ui-panelmenu').length) {
               $this.removeFocusedItem();
            }
        });
    },

    /**
     * Collapses all siblings of the given header column.
     * @private
     * @param {JQuery} header The header column that was clicked. 
     */
    collapseActiveSibling: function(header) {
        this.collapseRootSubmenu(header.parent().siblings().children('.ui-panelmenu-header.ui-state-active').eq(0));
    },

    /**
     * Finds the next menu item to focus and highlight when the user presses the down arrow key.
     * @param {JQuery} item An item where to start the search.
     * @return {JQuery | null} The found item that should receive focus, or `null` if no item was found.
     * @private
     */
    searchDown: function(item) {
        var nextOfParent = item.closest('ul').parent('li').next(),
        itemToFocus = null;

        if(nextOfParent.length) {
            itemToFocus = nextOfParent;
        }
        else if(item.closest('ul').parent('li').length === 0){
            itemToFocus = item;
        }
        else {
            itemToFocus = this.searchDown(item.closest('ul').parent('li'));
        }

        return itemToFocus;
    },

    /**
     * Finds the first child menu item of the given content element.
     * @param {JQuery} content Some content element of this panel menu.
     * @return {JQuery} The first child menu item of the given content, with the class `.ui-menuitem`.
     * @private
     */
    getFirstItemOfContent: function(content) {
        return content.find('> .ui-menu-list > .ui-menuitem:visible:first-child');
    },

    /**
     * Finds the displayed text of the given menu item.
     * @param {JQuery} item A menu item of this panel menu.
     * @return {string} The displayed text of the given menu item, not including the text of sub menu items.
     */
    getItemText: function(item) {
        return item.find('> .ui-menuitem-link > span.ui-menuitem-text');
    },

    /**
     * Puts focus on the given menu item.
     * @param {JQuery} item A menu item to focus. 
     */
    focusItem: function(item) {
        this.removeFocusedItem();
        this.getItemText(item).addClass('ui-menuitem-outline').trigger('focus');
        this.focusedItem = item;
    },

    /**
     * Callback invoked after the focused menu item receives a blur.
     * @private
     */
    removeFocusedItem: function() {
        if(this.focusedItem) {
            this.getItemText(this.focusedItem).removeClass('ui-menuitem-outline');
            this.focusedItem = null;
        }
    },

    /**
     * Checks whether the given menu items is currently expanded or collapsed.
     * @param {JQuery} item A menu item to check.
     * @return {boolean} `true` if the given menu item is expanded (children are shown), or `false` otherwise. 
     */
    isExpanded: function(item) {
        return item.children('ul.ui-menu-list').is(':visible');
    },

    /**
     * Collapses the given accordional panel, hiding the menu entries it contains.
     * @param {JQuery} header A menu panel to collapse.
     */
    collapseRootSubmenu: function(header) {
        var panel = header.next();

        header.attr('aria-expanded', false).removeClass('ui-state-active ui-corner-top').addClass('ui-state-hover ui-corner-all')
                            .children('.ui-icon').removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');

        panel.attr('aria-hidden', true).slideUp('normal', 'easeInOutCirc');

        this.removeAsExpanded(panel);
    },

    /**
     * Expands the given accordional panel, showing the menu entries it contains.
     * @param {JQuery} header A menu panel to collapse.
     * @param {boolean} [restoring] Whether this method was called from `restoreState`.
     */
    expandRootSubmenu: function(header, restoring) {
        var panel = header.next();

        header.attr('aria-expanded', true).addClass('ui-state-active ui-corner-top').removeClass('ui-state-hover ui-corner-all')
                .children('.ui-icon').removeClass('ui-icon-triangle-1-e').addClass('ui-icon-triangle-1-s');

        if(restoring) {
            panel.attr('aria-hidden', false).show();
        }
        else {
            panel.attr('aria-hidden', false).slideDown('normal', 'easeInOutCirc');

            this.addAsExpanded(panel);
        }
    },

    /**
     * Expands the given tree-like sub menu item, showing the sub menu entries it contains.
     * @param {JQuery} submenu A sub menu tree item to expand.
     * @param {boolean} [restoring] Whether this method was called from `restoreState`.
     */
    expandTreeItem: function(submenu, restoring) {
        var submenuLink = submenu.find('> .ui-menuitem-link');

        submenuLink.find('> .ui-menuitem-text').attr('aria-expanded', true);
        submenuLink.find('> .ui-panelmenu-icon').addClass('ui-icon-triangle-1-s');
        submenu.children('.ui-menu-list').show();

        if(!restoring) {
            this.addAsExpanded(submenu);
        }
    },

    /**
     * Collapses the given tree-like sub menu item, hiding the sub menu entries it contains.
     * @param {JQuery} submenu A sub menu tree item to collapse.
     */
    collapseTreeItem: function(submenu) {
        var submenuLink = submenu.find('> .ui-menuitem-link');

        submenuLink.find('> .ui-menuitem-text').attr('aria-expanded', false);
        submenuLink.find('> .ui-panelmenu-icon').removeClass('ui-icon-triangle-1-s');
        submenu.children('.ui-menu-list').hide();

        this.removeAsExpanded(submenu);
    },

    /**
     * Writes the UI state of this panel menu to an HTML5 Local Store. Used to preserve the state during AJAX updates as well as
     * between page reloads.
     * @private
     */
    saveState: function() {
        if(this.cfg.stateful) {
            var expandedNodeIds = this.expandedNodes.join(',');

            localStorage.setItem(this.stateKey, expandedNodeIds);
        }
    },

    /**
     * Read the UI state of this panel menu stored in an HTML5 Local Store and reapplies to this panel menu. Used to preserve the
     * state during AJAX updates as well as between page reloads.
     * @private
     */
    restoreState: function() {
        var expandedNodeIds = null;

        if(this.cfg.stateful) {
            expandedNodeIds = localStorage.getItem(this.stateKey);
        }

        if(expandedNodeIds) {
            this.collapseAll();
            this.expandedNodes = expandedNodeIds.split(',');

            for(var i = 0 ; i < this.expandedNodes.length; i++) {
                var element = $(PrimeFaces.escapeClientId(this.expandedNodes[i]));
                if(element.is('div.ui-panelmenu-content'))
                    this.expandRootSubmenu(element.prev(), true);
                else if(element.is('li.ui-menu-parent'))
                    this.expandTreeItem(element, true);
            }
        }
        else {
            this.expandedNodes = [];
            var activeHeaders = this.headers.filter('.ui-state-active'),
            activeTreeSubmenus = this.jq.find('.ui-menu-parent > .ui-menu-list:not(.ui-helper-hidden)');

            for(var i = 0; i < activeHeaders.length; i++) {
                this.expandedNodes.push(activeHeaders.eq(i).next().attr('id'));
            }

            for(var i = 0; i < activeTreeSubmenus.length; i++) {
                this.expandedNodes.push(activeTreeSubmenus.eq(i).parent().attr('id'));
            }
        }
    },

    /**
     * Callback invoked after a menu item was collapsed. Saves the current UI state in an HTML5 Local Store.
     * @param {JQuery} element Element that was collapsed.
     * @private
     */
    removeAsExpanded: function(element) {
        var id = element.attr('id');

        this.expandedNodes = $.grep(this.expandedNodes, function(value) {
            return value != id;
        });

        this.saveState();
    },

    /**
     * Callback invoked after a menu item was expanded. Saves the current UI state in an HTML5 Local Store.
     * @param {JQuery} element Element that was expanded.
     * @private
     */
    addAsExpanded: function(element) {
        this.expandedNodes.push(element.attr('id'));

        this.saveState();
    },

    /**
     * Deletes the UI state of this panel menu stored in an HTML5 Local Store.
     * @private
     */
    clearState: function() {
        if(this.cfg.stateful) {
            localStorage.removeItem(this.stateKey);
        }
    },

    /**
     * Collapses all menu panels that are currently expanded.
     */
    collapseAll: function() {
        this.headers.filter('.ui-state-active').each(function() {
            var header = $(this);
            header.removeClass('ui-state-active').children('.ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e').removeClass('ui-icon-triangle-1-s');
            header.next().addClass('ui-helper-hidden');
        });

        this.jq.find('.ui-menu-parent > .ui-menu-list:not(.ui-helper-hidden)').each(function() {
            $(this).addClass('ui-helper-hidden').prev().children('.ui-panelmenu-icon').removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-e');
        });
    }

});

/**
 * __PrimeFaces TabMenu Widget__
 * 
 * TabMenu is a navigation component that displays menuitems as tabs.
 * 
 * @prop {JQuery} items The DOM elements for the tab menu entries.
 * 
 * @interface {PrimeFaces.widget.TabMenuCfg} cfg The configuration for the {@link  TabMenu| TabMenu 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.MenuCfg} cfg
 */
PrimeFaces.widget.TabMenu = PrimeFaces.widget.Menu.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.items = this.jq.find('> .ui-tabmenu-nav > li:not(.ui-state-disabled)');

        this.bindEvents();
        this.bindKeyEvents();
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        this.items.on('mouseover.tabmenu', function(e) {
                    var element = $(this);
                    if(!element.hasClass('ui-state-active')) {
                        element.addClass('ui-state-hover');
                    }
                })
                .on('mouseout.tabmenu', function(e) {
                    $(this).removeClass('ui-state-hover');
                });
    },

    /**
     * Sets up all keyboard event listeners that are required by this widget.
     * @private
     */
    bindKeyEvents: function() {
        /* For Keyboard accessibility and Screen Readers */
        this.items.attr('tabindex', 0);

        this.items.on('focus.tabmenu', function(e) {
            $(this).addClass('ui-menuitem-outline');
        })
        .on('blur.tabmenu', function(){
            $(this).removeClass('ui-menuitem-outline');
        })
        .on('keydown.tabmenu', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                var currentLink = $(this).children('a');
                currentLink.trigger('click');
                PrimeFaces.utils.openLink(e, currentLink);
            }
        });
    }
});

/**
 * __PrimeFaces Message Widget__
 * 
 * Message is a pre-skinned extended version of the standard JSF message component.
 * 
 * @interface {PrimeFaces.widget.MessageCfg} cfg The configuration for the {@link  Message| Message 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 {string} cfg.target Client ID of the target for which to show this message.
 */
PrimeFaces.widget.Message = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        
        var text = this.jq.children('.ui-message-error-detail').text();
        
        if(text) {
           var target = $(PrimeFaces.escapeClientId(this.cfg.target));
           
           if (this.cfg.tooltip) {
              target.data('tooltip', text);
           }
           
           target.attr('aria-describedby', this.id + '_error-detail');
        } 
   }
});
/**
 * __PrimeFaces NotificationBar Widget__
 * 
 * NotificationBar displays a multipurpose fixed positioned panel for notification.
 * 
 * @typedef {"slide" | "fade" | "none"} PrimeFaces.widget.NotificationBar.Effect Possible values for the effect applied
 * when the notification bar is shown or hidden.  
 * 
 * @typedef {"top" | "bottom"} PrimeFaces.widget.NotificationBar.Position Possible values for where the notification bar
 * is shown.
 * 
 * @typedef {"fast" | "normal" | "slow"} PrimeFaces.widget.NotificationBar.EffectSpeed Possible values for speed of the
 * effect when the notification bar is shown or hidden.
 * 
 * @interface {PrimeFaces.widget.NotificationBarCfg} cfg The configuration for the {@link  NotificationBar| NotificationBar 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.autoDisplay Whether the notification bar is shown by default on page load.
 * @prop {PrimeFaces.widget.NotificationBar.Effect} cfg.effect Effect applied when the notification bar is shown or
 * hidden.  
 * @prop {PrimeFaces.widget.NotificationBar.EffectSpeed} cfg.effectSpeed Speed of the effect when the notification bar
 * is shown or hidden.  
 * @prop {PrimeFaces.widget.NotificationBar.Position} cfg.position Position of the bar, either top or bottom.
 */
PrimeFaces.widget.NotificationBar = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        
        var _self = this;
	
        //relocate
        this.jq.css(this.cfg.position, '0px').appendTo($('body'));

        //display initially
        if(this.cfg.autoDisplay) {
            $(this.jq).css('display','block')
        }

        //bind events
        this.jq.children('.ui-notificationbar-close').on("click", function() {
            _self.hide();
        });
    },
    
    /**
     * Shows the notification bar.
     * 
     * The up-to-three arguments will be routed to jQuery as-is.
     * 
     * @param {any} [a1] First parameter passed through to jQuery UI.
     * @param {any} [a2] Second parameter passed through to jQuery UI.
     * @param {any} [a3] Third parameter passed through to jQuery UI.
     * 
     * @see http://api.jquery.com/slidedown/
     * @see http://api.jquery.com/fadein/
     * @see http://api.jquery.com/show/
     */
    show: function(a1, a2, a3) {
        if(this.cfg.effect === 'slide')
            $(this.jq).slideDown(a1, a2, a3);
        else if(this.cfg.effect === 'fade')
            $(this.jq).fadeIn(a1, a2, a3);
        else if(this.cfg.effect === 'none')
            $(this.jq).show(a1, a2, a3);
    },
    
    /**
     * Hides the notification bar.
     */
    hide: function() {
        if(this.cfg.effect === 'slide')
            $(this.jq).slideUp(this.cfg.effect);
        else if(this.cfg.effect === 'fade')
            $(this.jq).fadeOut(this.cfg.effect);
        else if(this.cfg.effect === 'none')
            $(this.jq).hide();
    },
    
    /**
     * Checks whether the notification bar is currently displayed.
     * @return {boolean} `true` if the notification bar is currently visible, `false` otherwise.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },

    /**
     * Shows the notification bar it is currently hidden, or hides it if it is currently displayed.
     */
    toggle: function() {
        if(this.isVisible())
            this.hide();
        else
            this.show();
    }
    
});

/**
 * __PrimeFaces Panel Widget__
 * 
 * Panel is a grouping component with content toggle, close and menu integration.
 * 
 * @typedef {"vertical" | "horizontal"} PrimeFaces.widget.Panel.ToggleOrientation When toggling a panel, defines whether
 * it slides up and down; or left and right.
 * 
 * @prop {JQuery} closer The DOM element for the icon that closes this panel.
 * @prop {JQuery} content The DOM element for the content of this panel.
 * @prop {JQuery} header The DOM element for the header of this panel. 
 * @prop {boolean} isTitlebarClicked Whether the title bar was recently clicked.
 * @prop {number} originalWidth The original width of this panel before it got collapsed.
 * @prop {JQuery} title The DOM element for the title text in the header of this panel. 
 * @prop {JQuery} toggler The DOM element for the icon that toggles this panel.
 * @prop {JQuery} toggleStateHolder The DOM element for the hidden input storing whether this panel is currently
 * expanded or collapsed.
 * @prop {JQuery} visibleStateHolder The DOM element for the hidden input storing whether this panel is currently
 * visible or hidden.
 * 
 * @interface {PrimeFaces.widget.PanelCfg} cfg The configuration for the {@link  Panel| Panel 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.closable Whether panel is closable.
 * @prop {number} cfg.closeSpeed Speed of closing effect in milliseconds
 * @prop {boolean} cfg.collapsed Whether the panel is initially collapsed.
 * @prop {boolean} cfg.hasMenu Whether this panel has a toggleable menu in the panel header. 
 * @prop {boolean} cfg.toggleable Whether the panel can be toggled (expanded and collapsed).
 * @prop {boolean} cfg.toggleableHeader Defines if the panel is toggleable by clicking on the whole panel header.
 * @prop {PrimeFaces.widget.Panel.ToggleOrientation} cfg.toggleOrientation Defines the orientation of the toggling.
 * @prop {number} cfg.toggleSpeed Speed of toggling in milliseconds.
 */
PrimeFaces.widget.Panel = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.header = this.jq.children('div.ui-panel-titlebar');
        this.title = this.header.children('span.ui-panel-title');
        this.content = $(this.jqId + '_content');

        this.bindEvents();
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        if(this.cfg.toggleable) {
            this.bindToggler();

            if(this.cfg.toggleableHeader) {
                this.header.on('click', function() {
                    if(!$this.isTitlebarClicked) {
                        $this.toggle();
                    }

                    $this.isTitlebarClicked = false;
                });
            }
        }

        if(this.cfg.closable) {
            this.bindCloser();
        }

        if(this.cfg.hasMenu) {
            $(this.jqId + '_menu').on('click.panel', function(e) {
                e.preventDefault();
            });
        }

        //visuals for action items
        this.header.find('.ui-panel-titlebar-icon').on('mouseover.panel',function() {
            $(this).addClass('ui-state-hover');
        }).on('mouseout.panel',function() {
            $(this).removeClass('ui-state-hover');
        }).on('click.panel', function(e) {
            var href = $(this).attr('href');
            if(!href || href == '#') {
                e.preventDefault();
            }

            $this.isTitlebarClicked = true;
        });
    },

    /**
     * Expands this panel if it is currently collapsed, or collapses it if it is currently expanded.
     */
    toggle: function() {
        if(this.cfg.collapsed) {
            this.expand();
            PrimeFaces.invokeDeferredRenders(this.id);
        }
        else {
            this.collapse();
        }
    },

    /**
     * Expands this panel, if not already expanded.
     */
    expand: function() {
        this.toggleState(false, 'ui-icon-plusthick', 'ui-icon-minusthick');

        if(this.cfg.toggleOrientation === 'vertical')
            this.slideDown();
        else if(this.cfg.toggleOrientation === 'horizontal')
            this.slideRight();
    },

    /**
     * Collapses this panel, if not already collapsed.
     */
    collapse: function() {
        this.toggleState(true, 'ui-icon-minusthick', 'ui-icon-plusthick');

        if(this.cfg.toggleOrientation === 'vertical')
            this.slideUp();
        else if(this.cfg.toggleOrientation === 'horizontal')
            this.slideLeft();
    },

    /**
     * Closes this panel by sliding it up.
     * @private
     */
    slideUp: function() {
        this.content.slideUp(this.cfg.toggleSpeed, 'easeInOutCirc');
    },

    /**
     * Opens this panel by sliding it down.
     * @private
     */
    slideDown: function() {
        this.content.slideDown(this.cfg.toggleSpeed, 'easeInOutCirc');
    },

    /**
     * Closes this panel by sliding it to the left.
     * @private
     */
    slideLeft: function() {
        var $this = this;

        this.originalWidth = this.jq.width();

        this.title.hide();
        this.toggler.hide();
        this.content.hide();

        this.jq.animate({
            width: '42px'
        }, this.cfg.toggleSpeed, 'easeInOutCirc', function() {
            $this.toggler.show();
            $this.jq.addClass('ui-panel-collapsed-h');
        });
    },

    /**
     * Opens this panel by sliding it to the right.
     * @private
     */
    slideRight: function() {
        var $this = this,
        expandWidth = this.originalWidth||'100%';

        this.toggler.hide();

        this.jq.animate({
            width: expandWidth
        }, this.cfg.toggleSpeed, 'easeInOutCirc', function() {
            $this.jq.removeClass('ui-panel-collapsed-h');
            $this.title.show();
            $this.toggler.show();

            $this.content.css({
                'visibility': 'visible'
                ,'display': 'block'
                ,'height': 'auto'
            });
        });
    },

    /**
     * Toggles the expansion state of this panel.
     * @private
     * @param {boolean} collapsed Whether the panel is now to be collapsed.
     * @param {JQuery} removeIcon Icon for closing this panel. 
     * @param {JQuery} addIcon Icon for opening this panel.
     */
    toggleState: function(collapsed, removeIcon, addIcon) {
        this.toggler.children('span.ui-icon').removeClass(removeIcon).addClass(addIcon);
        this.cfg.collapsed = collapsed;
        this.toggleStateHolder.val(collapsed);

        this.callBehavior('toggle');
    },

    /**
     * Closes this panel, if not already closed.
     */
    close: function() {
        if(this.visibleStateHolder) {
            this.visibleStateHolder.val(false);
        }

        var $this = this;
        this.jq.fadeOut(this.cfg.closeSpeed, function(e) {
            if($this.hasBehavior('close')) {
                $this.callBehavior('close');
            }
        });
    },

    /**
     * Shows this panel, if not already shown.
     */
    show: function() {
        var $this = this;
        this.jq.fadeIn(this.cfg.closeSpeed, function() {
            PrimeFaces.invokeDeferredRenders($this.id);
        });

        if(this.visibleStateHolder) {
            this.visibleStateHolder.val(true);
        }
    },

    /**
     * Sets up the event listeners for the button that toggles this panel between opened and closes.
     * @private
     */
    bindToggler: function() {
        var $this = this;

        this.toggler = $(this.jqId + '_toggler');
        this.toggleStateHolder = $(this.jqId + '_collapsed');

        this.toggler.on("click", function() {
            $this.toggle();

            return false;
        });
    },

    /**
     * Sets up the event listeners for the button that closes this panel.
     * @private
     */
    bindCloser: function() {
        var $this = this;

        this.closer = $(this.jqId + '_closer');
        this.visibleStateHolder = $(this.jqId + "_visible");

        this.closer.on("click", function(e) {
            $this.close();
            e.preventDefault();

            return false;
        });
    }

});
/**
 * __PrimeFaces OrderList Widget__
 * 
 * OrderList is used to sort a collection featuring drag&drop based reordering, transition effects and POJO support.
 * 
 * @prop {JQuery} input The DOM element for the hidden form field storing the current order of the items.
 * @prop {JQuery} items The DOM elements for the available items that can be reordered.
 * @prop {JQuery} list The DOM element for the container with the items.
 * 
 * @interface {PrimeFaces.widget.OrderListCfg} cfg The configuration for the {@link  OrderList| OrderList 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.disabled Whether this widget is disabled initially.
 * @prop {string} cfg.effect Name of animation to display.
 */
PrimeFaces.widget.OrderList = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.list = this.jq.find('.ui-orderlist-list'),
        this.items = this.list.children('.ui-orderlist-item');
        this.input = $(this.jqId + '_values');
        this.cfg.effect = this.cfg.effect||'fade';
        this.cfg.disabled = this.jq.hasClass('ui-state-disabled');
        var $this = this;

        if(!this.cfg.disabled) {
            this.generateItems();

            this.setupButtons();

            //Enable dnd
            this.list.sortable({
                revert: 1,
                placeholder: "ui-orderlist-item ui-state-highlight",
                forcePlaceholderSize: true,
                start: function(event, ui) {
                    PrimeFaces.clearSelection();
                }
                ,update: function(event, ui) {
                    $this.onDragDrop(event, ui);
                }
            });
            
            this.bindEvents();
        }
    },

    /**
     * Reads the current item order and stores it in a hidden form field.
     * @private
     */
    generateItems: function() {
        var $this = this;

        this.list.children('.ui-orderlist-item').each(function() {
            var item = $(this),
            itemValue = item.data('item-value'),
            option = $('<option selected="selected"></option>');

            option.prop('value', itemValue).text(itemValue);
            $this.input.append(option);
        });
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;
        
        if (PrimeFaces.env.browser.mobile) {
            var disabledSortable = function() {
                $this.list.sortable('disable');
                $this.items.css('touch-action', 'auto');
            };
            
            disabledSortable();
            
            this.items.on('touchend.orderList-mobile', function() {
                disabledSortable();
            })
            .on('click.orderList-mobile', function() {
                $this.list.sortable('enable');
            });
        }

        this.items.on('mouseover.orderList', function(e) {
            var element = $(this);

            if(!element.hasClass('ui-state-highlight'))
                $(this).addClass('ui-state-hover');
        })
        .on('mouseout.orderList', function(e) {
            var element = $(this);

            if(!element.hasClass('ui-state-highlight'))
                $(this).removeClass('ui-state-hover');
        })
        .on('mousedown.orderList', function(e) {
            var element = $(this),
            metaKey = (e.metaKey||e.ctrlKey);

            if(!metaKey) {
                element.removeClass('ui-state-hover').addClass('ui-state-highlight')
                .siblings('.ui-state-highlight').removeClass('ui-state-highlight');

                $this.fireItemSelectEvent(element, e);
            }
            else {
                if(element.hasClass('ui-state-highlight')) {
                    element.removeClass('ui-state-highlight');
                    $this.fireItemUnselectEvent(element);
                }
                else {
                    element.removeClass('ui-state-hover').addClass('ui-state-highlight');
                    $this.fireItemSelectEvent(element, e);
                }
            }
        });
    },

    /**
     * Sets up the buttons and corresponding event listeners for moving order list items up and down.
     * @private
     */
    setupButtons: function() {
        var $this = this;

        PrimeFaces.skinButton(this.jq.find('.ui-button'));

        this.jq.find(' .ui-orderlist-controls .ui-orderlist-button-move-up').on("click", function() {$this.moveUp($this.sourceList);});
        this.jq.find(' .ui-orderlist-controls .ui-orderlist-button-move-top').on("click", function() {$this.moveTop($this.sourceList);});
        this.jq.find(' .ui-orderlist-controls .ui-orderlist-button-move-down').on("click", function() {$this.moveDown($this.sourceList);});
        this.jq.find(' .ui-orderlist-controls .ui-orderlist-button-move-bottom').on("click", function() {$this.moveBottom($this.sourceList);});
    },

    /**
     * Callback that is invoked when an order list item was moved via drag and drop. Saves the new order of the items
     * and invokes the appropriate behaviors.
     * @private
     * @param {JQuery.TriggeredEvent} event The event that triggered the drag or drop.
     * @param {JQueryUI.SortableUIParams} ui The UI params as passed by JQuery UI to the event handler.
     */
    onDragDrop: function(event, ui) {
        ui.item.removeClass('ui-state-highlight');
        this.saveState();
        this.fireReorderEvent();
    },

    /**
     * Saves the current value of this order list, i.e. the order of the items.  The value is saved in a hidden form
     * field.
     * @private
     */
    saveState: function() {
        this.input.children().remove();

        this.generateItems();
    },

    /**
     * Moves the selected order list items up by one, as if the `move up` button were pressed.
     */
    moveUp: function() {
        var $this = this,
        selectedItems = $this.list.children('.ui-orderlist-item.ui-state-highlight'),
        itemsToMoveCount = selectedItems.length,
        movedItemsCount = 0,
        hasFirstChild = selectedItems.is(':first-child');

        if(hasFirstChild) {
            return;
        }

        selectedItems.each(function() {
            var item = $(this);

            if(!item.is(':first-child')) {
                item.hide($this.cfg.effect, {}, 'fast', function() {
                    item.insertBefore(item.prev()).show($this.cfg.effect, {}, 'fast', function() {
                        movedItemsCount++;

                        if(itemsToMoveCount === movedItemsCount) {
                            $this.saveState();
                            $this.fireReorderEvent();
                        }
                    });
                });
            }
            else {
                itemsToMoveCount--;
            }
        });
    },

    /**
     * Moves the selected order list items to the top, as if the `move to top` button were pressed.
     */
    moveTop: function() {
        var $this = this,
        selectedItems = $this.list.children('.ui-orderlist-item.ui-state-highlight'),
        itemsToMoveCount = selectedItems.length,
        movedItemsCount = 0,
        hasFirstChild = selectedItems.is(':first-child'),
        firstSelectedItemIndex = selectedItems.eq(0).index();

        if(hasFirstChild) {
            return;
        }

        selectedItems.each(function(index) {
            var item = $(this),
                currentIndex = (index === 0) ? 0 : (item.index() - firstSelectedItemIndex);

            if(!item.is(':first-child')) {
                item.hide($this.cfg.effect, {}, 'fast', function() {
                    item.insertBefore($this.list.children('.ui-orderlist-item').eq(currentIndex)).show($this.cfg.effect, {}, 'fast', function(){
                        movedItemsCount++;

                        if(itemsToMoveCount === movedItemsCount) {
                            $this.saveState();
                            $this.fireReorderEvent();
                        }
                    });
                });
            }
            else {
                itemsToMoveCount--;
            }
        });
    },

    /**
     * Moves the selected order list items down by one, as if the `move down` button were pressed.
     */
    moveDown: function() {
        var $this = this,
        selectedItems = $($this.list.children('.ui-orderlist-item.ui-state-highlight').get().reverse()),
        itemsToMoveCount = selectedItems.length,
        movedItemsCount = 0,
        hasFirstChild = selectedItems.is(':last-child');

        if(hasFirstChild) {
            return;
        }

        selectedItems.each(function() {
            var item = $(this);

            if(!item.is(':last-child')) {
                item.hide($this.cfg.effect, {}, 'fast', function() {
                    item.insertAfter(item.next()).show($this.cfg.effect, {}, 'fast', function() {
                        movedItemsCount++;

                        if(itemsToMoveCount === movedItemsCount) {
                            $this.saveState();
                            $this.fireReorderEvent();
                        }
                    });
                });
            }
            else {
                itemsToMoveCount--;
            }
        });
    },

    /**
     * Moves the selected order list items to the bottom, as if the `move to bottom` button were pressed.
     */
    moveBottom: function() {
        var $this = this,
        selectedItems = $($this.list.children('.ui-orderlist-item.ui-state-highlight').get().reverse()),
        itemsToMoveCount = selectedItems.length,
        movedItemsCount = 0,
        hasFirstChild = selectedItems.is(':last-child'),
        lastSelectedItemIndex = selectedItems.eq(0).index(),
        itemsLength = this.items.length;

        if(hasFirstChild) {
            return;
        }

        selectedItems.each(function(index) {
            var item = $(this),
                currentIndex = (index === 0) ? itemsLength - 1 : (item.index() - lastSelectedItemIndex) - 1;

            if(!item.is(':last-child')) {
                item.hide($this.cfg.effect, {}, 'fast', function() {
                    item.insertAfter($this.list.children('.ui-orderlist-item').eq(currentIndex)).show($this.cfg.effect, {}, 'fast', function() {
                        movedItemsCount++;

                        if(itemsToMoveCount === movedItemsCount) {
                            $this.saveState();
                            $this.fireReorderEvent();
                        }
                    });
                });
            }
            else {
                itemsToMoveCount--;
            }
        });
    },

    /**
     * Invokes the appropriate behavior for when an item of the order list was selected.
     * @private
     * @param {JQuery} item The item that was selected.
     * @param {JQuery.TriggeredEvent} e The event that occurred.
     */
    fireItemSelectEvent: function(item, e) {
        if(this.hasBehavior('select')) {
            var ext = {
                params: [
                    {name: this.id + '_itemIndex', value: item.index()},
                    {name: this.id + '_metaKey', value: e.metaKey},
                    {name: this.id + '_ctrlKey', value: e.ctrlKey}
                ]
            };

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

    /**
     * Invokes the appropriate behavior for when an item of the order list was unselected.
     * @private
     * @param {JQuery} item The item that was unselected.
     */
    fireItemUnselectEvent: function(item) {
        if(this.hasBehavior('unselect')) {
            var ext = {
                params: [
                    {name: this.id + '_itemIndex', value: item.index()}
                ]
            };

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

    /**
     * Invokes the appropriate behavior for when the order list was reordered.
     * @private
     */
    fireReorderEvent: function() {
        if(this.hasBehavior('reorder')) {
            this.callBehavior('reorder');
        }
    }

});
/**
 * __PrimeFaces OutputPanel Widget__
 * 
 * OutputPanel is a panel component with the ability for deferred loading.
 * 
 * @typedef {"load" | "visible"} PrimeFaces.widget.OutputPanel.DeferredMode Mode that indicates how the content of an
 * output panel is loaded:
 * - `load`: Loads the content directly after the page was loaded.
 * - `visible`: Loads the panel once it is visible, e.g. once the user scrolled down.
 * 
 * @interface {PrimeFaces.widget.OutputPanelCfg} cfg The configuration for the {@link  OutputPanel| OutputPanel 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.deferred Deferred mode loads the contents after page load to speed up page load.
 * @prop {PrimeFaces.widget.OutputPanel.DeferredMode} cfg.deferredMode Defines deferred loading mode, whether the
 * content is loaded directly after the page is done loading, or only once the user scrolled to the panel.
 * @prop {boolean} cfg.global When the content is loaded via AJAX, whether AJAX request triggers the global
 * `ajaxStatus`.
 */
PrimeFaces.widget.OutputPanel = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.cfg.global = this.cfg.global||false;

        if(this.cfg.deferred) {
            if(this.cfg.deferredMode === 'load') {
                this.loadContent();
            }
            else if(this.cfg.deferredMode === 'visible') {
                if(this.visible())
                    this.loadContent();
                else
                    this.bindScrollMonitor();
            }
        }
    },

    /**
     * Loads the content of this panel via AJAX, if dynamic loading is enabled.
     * @private
     */
    loadContent: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            async: true,
            ignoreAutoUpdate: true,
            global: false,
            params: [
                {name: this.id + '_load', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            $this.jq.html(content);
                        }
                    });

                return true;
            },
            onerror: function(xhr, status, errorThrown) {
                $this.jq.html('');
            }
        };

        if(this.hasBehavior('load')) {
            this.callBehavior('load', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Sets up the event listeners for handling scrolling.
     * @private
     */
    bindScrollMonitor: function() {
        var $this = this;

        PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_align', function() {
            if ($this.visible()) {
                PrimeFaces.utils.unbindScrollHandler($this, 'scroll.' + $this.id + '_align');
                $this.loadContent();
            }
        });
    },

    /**
     * Checks whether this panel is currently visible.
     * @return {boolean} `true` if this panel is currently visible, or `false` otherwise.
     */
    visible: function() {
        var win = $(window),
        scrollTop = win.scrollTop(),
        height = win.height(),
        top = this.jq.offset().top,
        bottom = top + this.jq.innerHeight();

        if((top >= scrollTop && top <= (scrollTop + height)) || (bottom >= scrollTop && bottom <= (scrollTop + height))) {
            return true;
        }
    }
});
/**
 * __PrimeFaces OverlayPanel Widget__
 * 
 * OverlayPanel is a generic panel component that can be displayed on top of other content.
 * 
 * @prop {JQuery} closerIcon The DOM element for the icon that closes the overlay panel.
 * @prop {JQuery} content The DOM element for the content of the overlay panel.
 * @prop {boolean} loaded When dynamic loading is enabled, whether the content was already loaded.
 * @prop {number} showTimeout The set-timeout timer ID of the timer used for showing the overlay panel.
 * @prop {JQuery} target The DOM element for the target component that triggers this overlay panel.
 * @prop {JQuery} targetElement The DOM element for the resolved target component that triggers this overlay panel.
 * @prop {number} targetZindex The z-index of the target component that triggers this overlay panel.
 * 
 * @interface {PrimeFaces.widget.OverlayPanelCfg} cfg The configuration for the {@link  OverlayPanel| OverlayPanel 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.DynamicOverlayWidgetCfg} cfg
 * 
 * @prop {string} cfg.appendTo Appends the overlayPanel to the given search expression.
 * @prop {string} cfg.at Position of the target relative to the panel.
 * @prop {boolean} cfg.dynamic `true` to load the content via AJAX when the overlay panel is opened, `false` to load
 * the content immediately.
 * @prop {string} cfg.hideEvent Event on target to hide the panel.
 * @prop {string} cfg.collision When the positioned element overflows the window in some direction, move it to an
 * alternative position. Similar to my and at, this accepts a single value or a pair for horizontal/vertical, e.g.,
 * `flip`, `fit`, `fit flip`, `fit none`.
 * @prop {boolean} cfg.dismissable When set `true`, clicking outside of the panel hides the overlay.
 * @prop {boolean} cfg.modal Specifies whether the document should be shielded with a partially transparent mask to
 * require the user to close the panel before being able to activate any elements in the document.
 * @prop {string} cfg.my Position of the panel relative to the target.
 * @prop {} cfg.onHide Client side callback to execute when panel is shown.
 * @prop {} cfg.onShow Client side callback to execute when panel is hidden.
 * @prop {boolean} cfg.showCloseIcon Displays a close icon to hide the overlay, default is `false`.
 * @prop {number} cfg.showDelay Delay in milliseconds applied when the overlay panel is shown.
 * @prop {string} cfg.showEvent Event on target to hide the panel.
 * @prop {string} cfg.target Search expression for target component to display panel next to.
 */
PrimeFaces.widget.OverlayPanel = PrimeFaces.widget.DynamicOverlayWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.content = this.jq.children('div.ui-overlaypanel-content');

        //configuration
        this.cfg.my = this.cfg.my||'left top';
        this.cfg.at = this.cfg.at||'left bottom';
        this.cfg.collision = this.cfg.collision||'flip';
        this.cfg.showEvent = this.cfg.showEvent||'click.ui-overlaypanel';
        this.cfg.hideEvent = this.cfg.hideEvent||'click.ui-overlaypanel';
        this.cfg.dismissable = (this.cfg.dismissable === false) ? false : true;
        this.cfg.showDelay = this.cfg.showDelay || 0;

        if(this.cfg.showCloseIcon) {
            this.closerIcon = $('<a href="#" class="ui-overlaypanel-close ui-state-default"><span class="ui-icon ui-icon-closethick"></span></a>')
                              .attr('aria-label', PrimeFaces.getAriaLabel('overlaypanel.CLOSE')).appendTo(this.jq);
        }

        this.bindCommonEvents();

        if (this.cfg.target) {
            this.target = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.target);
            this.bindTargetEvents();

            // set aria attributes
            this.target.attr({
                'aria-expanded': false,
                'aria-controls': this.id
            });

            //dialog support
            this.setupDialogSupport();
        }

        this.transition = PrimeFaces.utils.registerCSSTransition(this.jq, 'ui-connected-overlay');
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);

        // fix #4307
        this.loaded = false;

        // see #setupDialogSupport
        if (!this.cfg.appendTo) {
            PrimeFaces.utils.removeDynamicOverlay(this, this.jq, this.id, $(document.body));
        }
    },

    /**
     * @override
     * @inheritdoc
     */
    destroy: function() {
        this._super();

        // see #setupDialogSupport
        if (!this.cfg.appendTo) {
            PrimeFaces.utils.removeDynamicOverlay(this, this.jq, this.id, $(document.body));
        }
    },

    /**
     * Sets up the event listeners for the target component that triggers this overlay panel.
     * @private
     */
    bindTargetEvents: function() {
        var $this = this;

        //mark target and descandants of target as a trigger for a primefaces overlay
        this.target.data('primefaces-overlay-target', this.id).find('*').data('primefaces-overlay-target', this.id);

        //show and hide events for target
        if(this.cfg.showEvent === this.cfg.hideEvent) {
            var event = this.cfg.showEvent;

            this.target.on(event, function(e) {
                $this.toggle();
            });
        }
        else {
            var showEvent = this.cfg.showEvent + '.ui-overlaypanel',
            hideEvent = this.cfg.hideEvent + '.ui-overlaypanel';

            this.target.off(showEvent + ' ' + hideEvent).on(showEvent, function(e) {
                if(!$this.isVisible()) {
                    $this.show();
                    if(showEvent === 'contextmenu.ui-overlaypanel') {
                        e.preventDefault();
                    }
                }
            })
            .on(hideEvent, function(e) {
            	clearTimeout($this.showTimeout);
                if($this.isVisible()) {
                    $this.hide();
                }
            });
        }

        $this.target.off('keydown.ui-overlaypanel keyup.ui-overlaypanel')
        .on('keydown.ui-overlaypanel', PrimeFaces.utils.blockEnterKey)
        .on('keyup.ui-overlaypanel', function(e) {
            var keyCode = $.ui.keyCode, key = e.which;

            if(key === keyCode.ENTER) {
                $this.toggle();
                e.preventDefault();
            }
        });

    },

    /**
     * Sets up some common event listeners always required by this widget.
     * @private
     */
    bindCommonEvents: function() {
        var $this = this;

        if(this.cfg.showCloseIcon) {
            this.closerIcon.on('mouseover.ui-overlaypanel', function() {
                $(this).addClass('ui-state-hover');
            })
            .on('mouseout.ui-overlaypanel', function() {
                $(this).removeClass('ui-state-hover');
            })
            .on('click.ui-overlaypanel', function(e) {
                $this.hide();
                e.preventDefault();
            })
            .on('focus.ui-overlaypanel', function() {
                $(this).addClass('ui-state-focus');
            })
            .on('blur.ui-overlaypanel', function() {
                $(this).removeClass('ui-state-focus');
            });
        }
    },

    /**
     * Sets up all panel event listeners
     * @private
     */
    bindPanelEvents: function() {
        var $this = this;

        //hide overlay when mousedown is at outside of overlay
        if (this.cfg.dismissable && !this.cfg.modal) {
            this.hideOverlayHandler = PrimeFaces.utils.registerHideOverlayHandler(this, 'mousedown.' + this.id + '_hide', this.jq,
                function() { return $this.target; },
                function(e, eventTarget) {
                    if (!($this.jq.is(eventTarget) || $this.jq.has(eventTarget).length > 0 || eventTarget.closest('.ui-input-overlay').length > 0)) {
                        $this.hide();
                    }
                });
        }

        this.resizeHandler = PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_hide', this.jq, function() {
            $this.hide();
        });

        this.scrollHandler = PrimeFaces.utils.registerConnectedOverlayScrollHandler(this, 'scroll.' + this.id + '_hide', this.target, function() {
            $this.hide();
        });
    },

    /**
     * Unbind all panel event listeners
     * @private
     */
    unbindPanelEvents: function() {
        if (this.hideOverlayHandler) {
            this.hideOverlayHandler.unbind();
        }

        if (this.resizeHandler) {
            this.resizeHandler.unbind();
        }
    
        if (this.scrollHandler) {
            this.scrollHandler.unbind();
        }
    },

    /**
     * Brings up the overlay panel if it is currently hidden, or hides it if it is currently displayed.
     */
    toggle: function() {
        if (!this.isVisible()) {
            this.show();
        }
        else {
            clearTimeout(this.showTimeout);
            this.hide();
        }
    },

    /**
     * Brings up the overlay panel so that is displayed and visible.
     * @param {string | JQuery} [target] ID or DOM element of the target component that triggers this overlay panel.
     */
    show: function(target) {
    	var thisPanel = this;
        this.showTimeout = setTimeout(function() {
            if (!thisPanel.loaded && thisPanel.cfg.dynamic) {
                thisPanel.loadContents(target);
            }
            else {
                thisPanel._show(target);
            }
        }, this.cfg.showDelay);
    },

    /**
     * Makes the overlay panel visible.
     * @private
     * @param {string | JQuery} [target] ID or DOM element of the target component that triggers this overlay panel.
     */
    _show: function(target) {
        var $this = this;

        if (this.transition) {
            var showWithCSSTransition = function() {
                $this.transition.show({
                    onEnter: function() {
                        $this.jq.css('z-index', PrimeFaces.nextZindex());
                        $this.align(target);
                    },
                    onEntered: function() {
                        $this.bindPanelEvents();
                        $this.postShow();
    
                        if ($this.cfg.modal) {
                            $this.enableModality();
                        }
                    }
                });
            };

            var targetEl = this.getTarget(target);
            if (this.isVisible() && this.targetElement && !this.targetElement.is(targetEl)) {
                this.hide(function() {
                    showWithCSSTransition();
                });
            }
            else {
                showWithCSSTransition();
            }
        }
    },

    /**
     * Get new target element using selector param.
     * @private
     * @param {string | JQuery} [target] ID or DOM element of the target component that triggers this overlay panel.
     * @return {JQuery|null} DOM Element or null
     */
    getTarget: function(target) {
        if (target) {
            if (typeof target === 'string') {
                return $(document.getElementById(target));
            }
            else if (target instanceof $) {
                return target;
            }
        }
        else if (this.target) {
            return this.target;
        }

        return null;
    },

    /**
     * Aligns the overlay panel so that it is shown at the correct position.
     * @private
     * @param {string | JQuery} [target] ID or DOM element of the target component that triggers this overlay panel.
     */
    align: function(target) {
        var win = $(window),
        allowedNegativeValuesByParentOffset = this.jq.offsetParent().offset();

        this.targetElement = this.getTarget(target);
        if (this.targetElement) {
            this.targetZindex = this.targetElement.zIndex();
        }

        this.jq.css({'left':'', 'top':'', 'transform-origin': 'center top'})
                .position({
                    my: this.cfg.my
                    ,at: this.cfg.at
                    ,of: this.targetElement
                    ,collision: this.cfg.collision
                    ,using: function(pos, directions) {
                        if (pos.top < -allowedNegativeValuesByParentOffset.top) {
                            pos.top = -allowedNegativeValuesByParentOffset.top;
                        }
                        
                        if (pos.left < -allowedNegativeValuesByParentOffset.left) {
                            pos.left = -allowedNegativeValuesByParentOffset.left;
                        }

                        $(this).css('transform-origin', 'center ' + directions.vertical).css(pos);
                    }
                });

        var widthOffset = this.jq.width() - this.content.width();
        this.jq.css('max-width', win.width() - widthOffset + 'px');
    },

    /**
     * Hides this overlay panel so that it is not displayed anymore.
     * @param {JQuery} [callback] Custom callback that is invoked after this overlay panel was closed.
     */
    hide: function(callback) {
        if (this.transition) {
            var $this = this;

            this.transition.hide({
                onExit: function() {
                    $this.unbindPanelEvents();
                },
                onExited: function() {
                    if ($this.cfg.modal) {
                        $this.disableModality();
                    }

                    $this.postHide();

                    if (callback) {
                        callback();
                    }
                }
            });
        }
    },

    /**
     * Callback that is invoked after this overlay panel was opened.
     * @private
     */
    postShow: function() {

        this.callBehavior('show');

        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }

        this.applyFocus();

        if (this.target) {
            this.target.attr('aria-expanded', true);
        }
    },

    /**
     * Callback that is invoked after this overlay panel was closed.
     * @private
     */
    postHide: function() {
        this.callBehavior('hide');

        if(this.cfg.onHide) {
            this.cfg.onHide.call(this);
        }

        if (this.target) {
            this.target.attr('aria-expanded', false);
        }
    },

    /**
     * In case this overlay panel is inside a dialog widget, applies some CSS fixes so that this overlay panel is above
     * the dialog-
     * @private
     */
    setupDialogSupport: function() {
        if (this.target && this.target[0]) {
            var dialog = this.target[0].closest('.ui-dialog');
            if (dialog) {
                var $dialog = $(dialog);
                if ($dialog.length == 1) {
                    //set position as fixed to scroll with dialog
                    if($dialog.css('position') === 'fixed') {
                        this.jq.css('position', 'fixed');
                    }

                    //append to body if not already appended by user choice
                    if(!this.cfg.appendTo) {
                        this.jq.appendTo(document.body);
                    }
                }
            }
        }
    },

    /**
     * Loads the contents of this overlay panel dynamically via AJAX, if dynamic loading is enabled.
     * @private
     * @param {string | JQuery} [target] ID or DOM element of the target component that triggers this overlay panel.
     */
    loadContents: function(target) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                            this.loaded = true;
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this._show(target);
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Checks whether this overlay panel is currently visible.
     * @return {boolean} `true` if this overlay panel is currently displayed, or `false` otherwise.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },

    /**
     * Applies focus to the first focusable element of the content in the panel.
     */
    applyFocus: function() {
        this.jq.find(':not(:submit):not(:button):input:visible:enabled:first').trigger('focus');
    },

    /**
     * @override
     * @inheritdoc
     */
    enableModality: function() {
        this._super();

        if(this.targetElement) {
            this.targetElement.css('z-index', String(this.jq.css('z-index')));
        }
    },

    /**
     * @override
     * @inheritdoc
     */
    disableModality: function(){
        this._super();

        if(this.targetElement) {
            this.targetElement.css('z-index', String(this.targetZindex));
        }
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getModalTabbables: function(){
        var tabbables = this.jq.find(':tabbable');

        if (this.targetElement && this.targetElement.is(':tabbable')) {
            tabbables = tabbables.add(this.targetElement);
        }

        return tabbables;
    }
});

/**
 * __PrimeFaces Paginator Widget__
 * 
 * A widget for handling pagination that is usually used by other widget via composition, that is, they create and save
 * an instance of this widget during initialization. After you create a new instance of this paginator, you should set
 * the `paginate` property to an appropriate callback function.
 * 
 * ```javascript
 * const paginator = new PrimeFaces.widget.Paginator(paginatorCfg);
 * paginator.paginator = newState => {
 *  // handle pagination
 * };
 * ```
 * 
 * @typedef PrimeFaces.widget.Paginator.PaginateCallback A callback method that is invoked when the pagination state
 * changes, see {@link PaginatorCfg.paginate}.
 * @param {PrimeFaces.widget.Paginator.PaginationState} PrimeFaces.widget.Paginator.PaginateCallback.newState The new
 * values for the current page and the rows per page count. 
 * 
 * @interface {PrimeFaces.widget.Paginator.PaginationState} PaginatorState Represents a pagination state, that is, a
 * range of items that should be displayed.
 * @prop {number} PaginatorState.first 0-based index of the first item on the current page.
 * @prop {number} PaginatorState.rows The number of rows per page.
 * @prop {number} PaginatorState.page The current page, 0-based index.
 * 
 * @prop {JQuery} currentReport DOM element of the status text as configured by the `currentPageTemplate`.
 * @prop {JQuery} endLink DOM element of the link to the last page.
 * @prop {JQuery} firstLink DOM element of the link back to the first page.
 * @prop {JQuery} jtpInput INPUT element for selecting a page to navigate to (`jump to page`)
 * @prop {JQuery} jtpSelect SELECT element for selecting a page to navigate to (`jump to page`)
 * @prop {JQuery} nextLink DOM element of the link to the next page.
 * @prop {JQuery} pagesContainer DOM element of the container with the numbered page links.
 * @prop {JQuery} pageLinks DOM elements of each numbered page link.
 * @prop {JQuery} prevLink DOM element of the link back to the previous page.
 * @prop {JQuery} rppSelect SELECT element for selection the number of pages to display (`rows per page`).
 * 
 * @interface {PrimeFaces.widget.PaginatorCfg} cfg The configuration for the {@link  Paginator| Paginator 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.alwaysVisible `true` if the paginator should be displayed always, or `false` if it is allowed to
 * be hidden under some circumstances that depend on the widget that uses the paginator.
 * @prop {string} cfg.ariaPageLabel ARIA LABEL attribute for the page links.
 * @prop {string} cfg.currentPageTemplate Template for the paginator text. It may contain placeholders such as
 * `{currentPage}` or `{totalPages}`. 
 * @prop {number} cfg.page The current page, 0-based index.
 * @prop {number} cfg.pageCount The number of pages.
 * @prop {number} cfg.pageLinks The maximum number of page links to display (when there are many pages).
 * @prop {PrimeFaces.widget.Paginator.PaginateCallback} cfg.paginate A callback method that is invoked when the
 * pagination state changes, such as when the user selects a different page or changes the current rows per page count.
 * This property is usually provided by another widget that makes use of this paginator. You should use this callback to
 * perform any actions required to apply the new pagination state.
 * @prop {number} cfg.prevRows The number of rows per page for the dropdown.
 * @prop {number} cfg.rowCount Total number of rows (records) to be displayed.
 * @prop {number} cfg.rows The number of rows per page.
 */
PrimeFaces.widget.Paginator = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        //elements
        this.pagesContainer = this.jq.children('.ui-paginator-pages');
        this.pageLinks = this.pagesContainer.children('.ui-paginator-page');
        this.rppSelect = this.jq.children('.ui-paginator-rpp-options');
        this.jtpSelect = this.jq.children('.ui-paginator-jtp-select');
        this.jtpInput = this.jq.children('.ui-paginator-jtp-input');
        this.firstLink = this.jq.children('.ui-paginator-first');
        this.prevLink  = this.jq.children('.ui-paginator-prev');
        this.nextLink  = this.jq.children('.ui-paginator-next');
        this.endLink   = this.jq.children('.ui-paginator-last');
        this.currentReport = this.jq.children('.ui-paginator-current');

        //metadata
        this.cfg.rows = this.cfg.rows == 0 ? this.cfg.rowCount : this.cfg.rows;
        this.cfg.prevRows = this.cfg.rows;
        this.cfg.pageCount = Math.ceil(this.cfg.rowCount / this.cfg.rows)||1;
        this.cfg.pageLinks = this.cfg.pageLinks||10;
        this.cfg.currentPageTemplate = this.cfg.currentPageTemplate||'({currentPage} of {totalPages})';

        //aria message
        this.cfg.ariaPageLabel = PrimeFaces.getAriaLabel('paginator.PAGE');

        //event bindings
        this.bindEvents();
    },

    /**
     * Sets up all event listeners for this widget.
     * @private
     */
    bindEvents: function(){
        var $this = this;

        //visuals for first,prev,next,last buttons
        this.jq.children('a.ui-state-default').on('mouseover.paginator', function(){
            var item = $(this);
            if(!item.hasClass('ui-state-disabled')) {
                item.addClass('ui-state-hover');
            }
        })
        .on('mouseout.paginator', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('focus.paginator', function() {
            var item = $(this);
            if(!item.hasClass('ui-state-disabled')) {
                item.addClass('ui-state-focus');
            }
        })
        .on('blur.paginator', function() {
            $(this).removeClass('ui-state-focus');
        })
        .on('keydown.paginator', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER)) {
                $(this).trigger('click');
                e.preventDefault();
            }
        });

        //page links
        this.bindPageLinkEvents();

        //records per page selection
        PrimeFaces.skinSelect(this.rppSelect);
        this.rppSelect.on('change', function(e) {
            if(!$(this).hasClass("ui-state-disabled")){
                $this.setRowsPerPage($(this).val());
            }
        });

        //jump to page dropdown
        PrimeFaces.skinSelect(this.jtpSelect);
        this.jtpSelect.on('change', function(e) {
            if(!$(this).hasClass("ui-state-disabled")){
                $this.setPage(parseInt($(this).val()));
            }
        });

        //jump to page input
        PrimeFaces.skinInput(this.jtpInput);
        this.jtpInput.on('change', function(e) {
            if(!$(this).hasClass("ui-state-disabled")){
                var page = parseInt($(this).val());
                if (isNaN(page) || page > $this.cfg.pageCount || page < 1) {
                    // restore old value if invalid
                    $(this).val($this.cfg.page + 1);
                }
                else {
                    $this.setPage(page - 1);
                }
            }
        });

        //First page link
        this.firstLink.on("click", function(e) {
            PrimeFaces.clearSelection();

            if(!$(this).hasClass("ui-state-disabled")){
                $this.setPage(0);
            }

            e.preventDefault();
        });

        //Prev page link
        this.prevLink.on("click", function(e) {
            PrimeFaces.clearSelection();

            if(!$(this).hasClass("ui-state-disabled")){
                $this.setPage($this.cfg.page - 1);
            }

            e.preventDefault();
        });

        //Next page link
        this.nextLink.on("click", function(e) {
            PrimeFaces.clearSelection();

            if(!$(this).hasClass("ui-state-disabled")){
                $this.setPage($this.cfg.page + 1);
            }

            e.preventDefault();
        });

        //Last page link
        this.endLink.on("click", function(e) {
            PrimeFaces.clearSelection();

            if(!$(this).hasClass("ui-state-disabled")){
                $this.setPage($this.cfg.pageCount - 1);
            }

            e.preventDefault();
        });
    },

    /**
     * Sets up the event listeners for page link buttons.
     * @private
     */
    bindPageLinkEvents: function(){
        var $this = this,
        pageLinks = this.pagesContainer.children('.ui-paginator-page');

        pageLinks.each(function() {
            var link = $(this),
            pageNumber = parseInt(link.text());

            link.attr('aria-label', $this.cfg.ariaPageLabel.replace('{0}', (pageNumber)));
        });

        pageLinks.on('click.paginator', function(e) {
            var link = $(this),
            pageNumber = parseInt(link.text());

            if(!link.hasClass('ui-state-disabled')&&!link.hasClass('ui-state-active')) {
                $this.setPage(pageNumber - 1);
            }

            e.preventDefault();
        })
        .on('mouseover.paginator', function() {
            var item = $(this);
            if(!item.hasClass('ui-state-disabled')&&!item.hasClass('ui-state-active')) {
                item.addClass('ui-state-hover');
            }
        })
        .on('mouseout.paginator', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('focus.paginator', function() {
            $(this).addClass('ui-state-focus');
        })
        .on('blur.paginator', function() {
            $(this).removeClass('ui-state-focus');
        })
        .on('keydown.paginator', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER)) {
                $(this).trigger('click');
                e.preventDefault();
            }
        });
    },

    /**
     * Binds swipe events to this paginator to the JQ element passed in.
     * @private
     * @param {JQuery} owner the owner JQ element of the paginator
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} ownerConfig the owner configuration to check if touch enabled or not
     */
    bindSwipeEvents: function(owner, ownerConfig) {
        if (!PrimeFaces.env.isTouchable(ownerConfig)) {
            return;
        }
        var $this = this;
        owner.swipe({
            swipeLeft:function(event) {
                $this.prev();
            },
            swipeRight: function(event) {
                $this.next();
            },
            excludedElements: PrimeFaces.utils.excludedSwipeElements()
        });
    },

   /**
    * Removes all event listeners.
    * @private
    */
   unbindEvents: function() {
        var buttons = this.jq.children('a.ui-state-default');
        if (buttons.length > 0) {
            buttons.off();
        }
        var pageLinks = this.pagesContainer.children('.ui-paginator-page');
        if (pageLinks.length > 0) {
            pageLinks.off();
        }
    },

    /**
     * Updates the UI so that it reflects the current pagination state.
     * @private
     */
    updateUI: function() {
        //boundaries
        if(this.cfg.page === 0) {
            this.disableElement(this.firstLink);
            this.disableElement(this.prevLink);
        }
        else {
            this.enableElement(this.firstLink);
            this.enableElement(this.prevLink);
        }

        if(this.cfg.page === (this.cfg.pageCount - 1)) {
            this.disableElement(this.nextLink);
            this.disableElement(this.endLink);
        }
        else {
            this.enableElement(this.nextLink);
            this.enableElement(this.endLink);
        }

        //current page report
        var startRecord = (this.cfg.rowCount === 0) ? 0 : (this.cfg.page * this.cfg.rows) + 1,
        endRecord = (this.cfg.page * this.cfg.rows) + this.cfg.rows;
        if(endRecord > this.cfg.rowCount) {
            endRecord = this.cfg.rowCount;
        }

        var text = this.cfg.currentPageTemplate
            .replace("{currentPage}", this.cfg.page + 1)
            .replace("{totalPages}", this.cfg.pageCount)
            .replace("{totalRecords}", this.cfg.rowCount)
            .replace("{startRecord}", startRecord)
            .replace("{endRecord}", endRecord);
        this.currentReport.text(text);

        //rows per page dropdown
        if(this.cfg.prevRows !== this.cfg.rows) {
            this.rppSelect.filter(':not(.ui-state-focus)').children('option').filter('option[value="' + $.escapeSelector(this.cfg.rows) + '"]').prop('selected', true);
            this.cfg.prevRows = this.cfg.rows;
        }

        //jump to page dropdown
        if(this.jtpSelect.length > 0) {
            if(this.jtpSelect[0].options.length != this.cfg.pageCount){
                var jtpOptions = '';
                for(var i = 0; i < this.cfg.pageCount; i++) {
                    jtpOptions += '<option value="' + i + '">' + (i + 1) + '</option>';
                }

                // GitHub #6929: performance improvement not using JQ html()
                this.jtpSelect[0].innerHTML = jtpOptions;
            }
            this.jtpSelect.children('option[value=' + (this.cfg.page) + ']').prop('selected','selected');
        }

        //jump to page input
        if(this.jtpInput.length > 0) {
            this.jtpInput.val(this.cfg.page + 1);
        }

        //page links
        this.updatePageLinks();
    },

    /**
     * Updates the UI of page link button so that they reflect the current pagination state.
     * @private
     */
    updatePageLinks: function() {
        var start, end, delta,
        focusedElement = $(document.activeElement),
        focusContainer;

        if(focusedElement.hasClass('ui-paginator-page')) {
            var pagesContainerIndex = this.pagesContainer.index(focusedElement.parent());
            if(pagesContainerIndex >= 0) {
                focusContainer = this.pagesContainer.eq(pagesContainerIndex);
            }
        }

        //calculate visible page links
        this.cfg.pageCount = Math.ceil(this.cfg.rowCount / this.cfg.rows)||1;
        var visiblePages = Math.min(this.cfg.pageLinks, this.cfg.pageCount);

        //calculate range, keep current in middle if necessary
        start = Math.max(0, Math.ceil(this.cfg.page - ((visiblePages) / 2)));
        end = Math.min(this.cfg.pageCount - 1, start + visiblePages - 1);

        //check when approaching to last page
        delta = this.cfg.pageLinks - (end - start + 1);
        start = Math.max(0, start - delta);

        //update dom
        this.pagesContainer.children().remove();
        for(var i = start; i <= end; i++) {
            var styleClass = 'ui-paginator-page ui-state-default ui-corner-all',
            ariaLabel = this.cfg.ariaPageLabel.replace('{0}', (i+1));

            if(this.cfg.page == i) {
                styleClass += " ui-state-active";
            }

            this.pagesContainer.append('<a class="' + styleClass + '" aria-label="' + ariaLabel + '" tabindex="0" href="#">' + (i + 1) + '</a>');
        }

        if(focusContainer) {
            focusContainer.children().filter('.ui-state-active').trigger('focus');
        }

        this.bindPageLinkEvents();
    },

    /**
     * Switches this pagination to the given page.
     * @param {number} p 0-based index of the page to switch to.
     * @param {boolean} [silent=false] `true` to not invoke any event listeners, `false` otherwise. 
     */
    setPage: function(p, silent) {
        if(p >= 0 && p < this.cfg.pageCount && this.cfg.page != p){
            var newState = {
                first: this.cfg.rows * p,
                rows: this.cfg.rows,
                page: p
            };

            if(silent) {
                this.cfg.page = p;
                this.updateUI();
            }
            else {
                this.cfg.paginate.call(this, newState);
            }
        }
    },

    /**
     * Modifies the number of rows that are shown per page.
     * @param {number} rpp Number of rows per page to set.
     */
    setRowsPerPage: function(rpp) {
        if (rpp === '*') {
            this.cfg.rows = this.cfg.rowCount;
            this.cfg.pageCount = 1;
            this.cfg.page = 0;

            var newState = {
                first: 0,
                rows: rpp,
                page: this.cfg.page
            };

            this.cfg.paginate.call(this, newState);
        }
        else {
            var first = this.cfg.rows * this.cfg.page;
            this.cfg.rows = parseInt(rpp);
            var page = parseInt(first / this.cfg.rows);

            this.cfg.pageCount = Math.ceil(this.cfg.rowCount / this.cfg.rows);
            this.cfg.page = -1;

            this.setPage(page);
        }
    },

    /**
     * Modifies the total number of items that are available, and switches to the first page.
     * @param {number} value The total number of items to set.
     */
    setTotalRecords: function(value) {
        this.cfg.rowCount = value;
        this.cfg.pageCount = Math.ceil(value / this.cfg.rows)||1;
        this.cfg.page = 0;
        this.updateUI();
    },

    /**
     * Modifies the total number of items that are available.
     * @param {number} value The total number of items to set.
     * @private
     */
    updateTotalRecords: function(value) {
        this.cfg.rowCount = value;
        this.cfg.pageCount = Math.ceil(value / this.cfg.rows)||1;
        this.updateUI();
    },

    /**
     * Finds the index of the page that is currently displayed.
     * @return {number} 0-based index of the current page.
     */
    getCurrentPage: function() {
        return this.cfg.page;
    },

    /**
     * Finds the index of the item that is shown first on the current page.
     * @return {number} 0-based index of the first item on the current page.
     */
    getFirst: function() {
        return (this.cfg.rows * this.cfg.page);
    },

    /**
     * Finds the current number of rows per page.
     * @return {number} The number of rows per page.
     */
    getRows: function() {
        return this.cfg.rows;
    },

    /**
     * Calculates the required height of the container with the items of the current page.
     * @private
     * @param {number} margin Additional margin in pixels to consider.
     * @return {number} The height of the items container in pixels
     */
    getContainerHeight: function(margin) {
        var height = 0;

        for(var i = 0; i < this.jq.length; i++) {
            height += this.jq.eq(i).outerHeight(margin);
        }

        return height;
    },

    /**
     * Disables one of the items of this pagination.
     * @private
     * @param {JQuery} element Element to disabled.
     */
    disableElement: function(element) {
        element.removeClass('ui-state-hover ui-state-focus ui-state-active').addClass('ui-state-disabled').attr('tabindex', -1);
        element.removeClass('ui-state-hover ui-state-focus ui-state-active').addClass('ui-state-disabled').attr('tabindex', -1);
    },

    /**
     * Enables one of the items of this pagination.
     * @private
     * @param {JQuery} element Element to disabled.
     */
    enableElement: function(element) {
        element.removeClass('ui-state-disabled').attr('tabindex', 0);
    },

    /**
     * Switches to the next page. Does nothing when this pagination is already on the last page.
     */
    next: function() {
        this.setPage(this.cfg.page + 1);
    },

    /**
     * Switches to the previous page. Does nothing when this pagination is already on the first page.
     */
    prev: function() {
        this.setPage(this.cfg.page - 1);
    }
});

/**
 * __PrimeFaces PickList Widget__
 *
 * PickList is used for transferring data between two different collections.
 *
 * @typedef {"source" | "target"} PrimeFaces.widget.PickList.ListName The type for the two lists comprising the pick
 * list, i.e. whether a list contain the source or target items.
 *
 * @typedef {"command" | "dblclick" | "dragdrop"} PrimeFaces.widget.PickList.TransferType Indicates how an item was
 * transferred from one list to the other.
 * - `command`: The item was transferred as a result of the user clicking one of the command buttons next to the lists.
 * - `dblclick`: The item was transferred as a result of a double click by the user.
 * - `dragdrop`:  The item was transferred as a result of a drag&drop interaction by the user.
 *
 * @typedef {"startsWith" |  "contains" |  "endsWith" | "custom"} PrimeFaces.widget.PickList.FilterMatchMode
 * Available modes for filtering the options of a pick list. When `custom` is set, a `filterFunction` must be specified.
 *
 * @typedef PrimeFaces.widget.PickList.FilterFunction A function for filtering the options of a pick list box.
 * @param {string} PrimeFaces.widget.PickList.FilterFunction.itemLabel The label of the currently selected text.
 * @param {string} PrimeFaces.widget.PickList.FilterFunction.filterValue The value to search for.
 * @return {boolean} PrimeFaces.widget.PickList.FilterFunction `true` if the item label matches the filter value, or
 * `false` otherwise.
 *
 * @typedef PrimeFaces.widget.PickList.OnTransferCallback Callback that is invoked when items are transferred from one
 * list to the other. See also {@link PickListCfg.onTransfer}.
 * @param {PrimeFaces.widget.PickList.TransferData} PrimeFaces.widget.PickList.OnTransferCallback.transferData Details
 * about the pick list item that was transferred.
 *
 * @interface {PrimeFaces.widget.PickList.TransferData} TransferData Callback that is invoked when an item was
 * transferred from one list to the other.
 * @prop {JQuery} TransferData.items Items that were transferred from one list to the other.
 * @prop {JQuery} TransferData.from List from which the items were transferred.
 * @prop {JQuery} TransferData.to List to which the items were transferred.
 * @prop {PrimeFaces.widget.PickList.TransferType} TransferData.type Type of the action that caused the items to be
 * transferred.
 *
 * @prop {JQuery} ariaRegion The DOM element for the aria region with the `aria-*` attributes
 * @prop {JQuery} checkboxes The DOM elements for the checkboxes next to each pick list item.
 * @prop {boolean} checkboxClick UI state indicating whether a checkbox was just clicked.
 * @prop {JQuery} cursorItem The currently selected item.
 * @prop {boolean} dragging Whether the user is currently transferring an item via drag&drop.
 * @prop {number} filterTimeout The set-timeout timer ID of the timer for the delay when filtering the source or target
 * list.
 * @prop {PrimeFaces.widget.PickList.FilterFunction} filterMatcher The filter that was selected and is currently used.
 * @prop {Record<PrimeFaces.widget.PickList.FilterMatchMode, PrimeFaces.widget.PickList.FilterFunction>} filterMatchers
 * Map between the available filter types and the filter implementation.
 * @prop {JQuery} focusedItem The DOM element for the currently focused pick list item, if any.
 * @prop {JQuery} items The DOM elements for the pick list items in the source and target list.
 * @prop {PrimeFaces.widget.PickList.ListName} itemListName When sorting items: to which list the items belong.
 * @prop {JQuery} sourceFilter The DOM element for the filter input for the source list.
 * @prop {JQuery} sourceInput The DOM element for the hidden input storing the value of the source list.
 * @prop {JQuery} sourceList The DOM element for the source list.
 * @prop {} targetFilter The DOM element for the filter input for the target list.
 * @prop {} targetInput The DOM element for the hidden input storing the value of the target list.
 * @prop {JQuery} targetList The DOM element for the target list.
 *
 * @interface {PrimeFaces.widget.PickListCfg} cfg The configuration for the {@link  PickList| PickList 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.disabled Whether this pick list is initially disabled.
 * @prop {string} cfg.effect Name of the animation to display.
 * @prop {string} cfg.effectSpeed Speed of the animation.
 * @prop {boolean} cfg.escapeValue Whether the item values are escaped for HTML.
 * @prop {number} cfg.filterDelay Delay to wait in milliseconds before sending each filter query. Default is `300`.
 * @prop {string} cfg.filterEvent Client side event to invoke picklist filtering for input fields. Default is `keyup`.
 * @prop {PrimeFaces.widget.PickList.FilterFunction} cfg.filterFunction A custom filter function that is used when
 * `filterMatchMode` is set to `custom`.
 * @prop {PrimeFaces.widget.PickList.FilterMatchMode} cfg.filterMatchMode Mode of the filter. When set to `custom, a
 * `filterFunction` must be specified.
 * @prop {PrimeFaces.widget.PickList.OnTransferCallback} cfg.onTransfer Callback that is invoked when items are
 * transferred from one list to the other.
 * @prop {boolean} cfg.showCheckbox When true, a checkbox is displayed next to each item.
 * @prop {boolean} cfg.showSourceControls Specifies visibility of reorder buttons of source list.
 * @prop {boolean} cfg.showTargetControls Specifies visibility of reorder buttons of target list.
 * @prop {string} cfg.tabindex Position of the element in the tabbing order.
 */
PrimeFaces.widget.PickList = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.sourceList = this.jq.find('ul.ui-picklist-source');
        this.targetList = this.jq.find('ul.ui-picklist-target');
        this.sourceInput = $(this.jqId + '_source');
        this.targetInput = $(this.jqId + '_target');
        this.items = this.jq.find('.ui-picklist-item:not(.ui-state-disabled)');
        if(this.cfg.showCheckbox) {
            this.checkboxes = this.items.find('div.ui-chkbox > div.ui-chkbox-box');
        }
        this.focusedItem = null;
        this.ariaRegion = $(this.jqId + '_ariaRegion');

        var sourceCaption = this.sourceList.prev('.ui-picklist-caption'),
            targetCaption = this.targetList.prev('.ui-picklist-caption');

        if(sourceCaption.length) {
            var captionText = sourceCaption.text();

            this.sourceList.attr('aria-label', captionText);
            this.sourceInput.attr('title', captionText);
        }

        if(targetCaption.length) {
            var captionText = targetCaption.text();

            this.targetList.attr('aria-label', captionText);
            this.targetInput.attr('title', captionText);
        }

        this.setTabIndex();

        //generate input options
        this.generateItems(this.sourceList, this.sourceInput);
        this.generateItems(this.targetList, this.targetInput);

        if(this.cfg.disabled) {
            $(this.jqId + ' li.ui-picklist-item').addClass('ui-state-disabled');
            $(this.jqId + ' button').attr('disabled', 'disabled').addClass('ui-state-disabled');
            $(this.jqId + ' .ui-picklist-filter-container').addClass('ui-state-disabled').children('input').attr('disabled', 'disabled');
        }
        else {
            var $this = this,
                reordered = true;

            //Sortable lists
            $(this.jqId + ' ul').sortable({
                cancel: '.ui-state-disabled,.ui-chkbox-box',
                connectWith: this.jqId + ' .ui-picklist-list',
                revert: 1,
                helper: 'clone',
                placeholder: "ui-picklist-item ui-state-highlight",
                forcePlaceholderSize: true,
                update: function(event, ui) {
                    $this.unselectItem(ui.item);

                    $this.saveState();
                    if(reordered) {
                        $this.fireReorderEvent();
                        reordered = false;
                    }
                },
                receive: function(event, ui) {
                    $this.fireTransferEvent(ui.item, ui.sender, ui.item.parents('ul.ui-picklist-list:first'), 'dragdrop');
                },

                start: function(event, ui) {
                    $this.itemListName = $this.getListName(ui.item);
                    $this.dragging = true;
                },

                stop: function(event, ui) {
                    $this.dragging = false;
                },

                beforeStop:function(event, ui) {
                    if($this.itemListName !== $this.getListName(ui.item)) {
                        reordered = false;
                    }
                    else {
                        reordered = true;
                    }
                }
            });

            this.bindItemEvents();

            this.bindButtonEvents();

            this.bindFilterEvents();

            this.bindKeyEvents();

            this.updateButtonsState();

            this.updateListRole();
        }
    },

    /**
     * Sets up the event listeners for selecting and transferring pick list items.
     * @private
     */
    bindItemEvents: function() {
        var $this = this;

        this.items.on('mouseover.pickList', function(e) {
            $(this).addClass('ui-state-hover');
        })
        .on('mouseout.pickList', function(e) {
            $(this).removeClass('ui-state-hover');
        })
        .on('click.pickList', function(e) {
            //stop propagation
            if($this.checkboxClick||$this.dragging) {
                $this.checkboxClick = false;
                return;
            }

            var item = $(this),
            parentList = item.parent(),
            metaKey = (e.metaKey||e.ctrlKey);

            if(!e.shiftKey) {
                if(!metaKey) {
                    $this.unselectAll();
                }

                if(metaKey && item.hasClass('ui-state-highlight')) {
                    $this.unselectItem(item, true);
                }
                else {
                    $this.selectItem(item, true);
                    $this.cursorItem = item;
                }
            }
            else {
                $this.unselectAll();

                if($this.cursorItem && ($this.cursorItem.parent().is(item.parent()))) {
                    var currentItemIndex = item.index(),
                    cursorItemIndex = $this.cursorItem.index(),
                    startIndex = (currentItemIndex > cursorItemIndex) ? cursorItemIndex : currentItemIndex,
                    endIndex = (currentItemIndex > cursorItemIndex) ? (currentItemIndex + 1) : (cursorItemIndex + 1);

                    for(var i = startIndex ; i < endIndex; i++) {
                        var it = parentList.children('li.ui-picklist-item').eq(i);

                        if(it.is(':visible')) {
                            if(i === (endIndex - 1))
                                $this.selectItem(it, true);
                            else
                                $this.selectItem(it);
                        }
                    }
                }
                else {
                    $this.selectItem(item, true);
                    $this.cursorItem = item;
                }
            }

            /* For keyboard navigation */
            $this.removeOutline();
            $this.focusedItem = item;
            parentList.trigger('focus.pickList');
        })
        .on('dblclick.pickList', function() {
            var item = $(this);

            if($(this).parent().hasClass('ui-picklist-source'))
                $this.transfer(item, $this.sourceList, $this.targetList, 'dblclick');
            else
                $this.transfer(item, $this.targetList, $this.sourceList, 'dblclick');

            /* For keyboard navigation */
            $this.removeOutline();
            $this.focusedItem = null;

            PrimeFaces.clearSelection();
        });

        if(this.cfg.showCheckbox) {
            this.checkboxes.on('mouseenter.pickList', function(e) {
                $(this).addClass('ui-state-hover');
            })
            .on('mouseleave.pickList', function(e) {
                $(this).removeClass('ui-state-hover');
            })
            .on('click.pickList', function(e) {
                $this.checkboxClick = true;

                var item = $(this).closest('li.ui-picklist-item');
                if(item.hasClass('ui-state-highlight')) {
                    $this.unselectItem(item, true);
                }
                else {
                    $this.selectItem(item, true);
                }
                $this.focusedItem = item;
            });
        }
    },

    /**
     * Sets up the keyboard event listeners for navigating the pick list via keyboard keys.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this,
            listSelector = 'ul.ui-picklist-source, ul.ui-picklist-target';

        this.jq.off('focus.pickList blur.pickList keydown.pickList', listSelector).on('focus.pickList', listSelector, null, function(e) {
            var list = $(this),
                activeItem = $this.focusedItem||list.children('.ui-state-highlight:visible:first');
            if(activeItem.length) {
                $this.focusedItem = activeItem;
            }
            else {
                $this.focusedItem = list.children('.ui-picklist-item:visible:first');
            }

            setTimeout(function() {
                if ($this.focusedItem) {
                    PrimeFaces.scrollInView(list, $this.focusedItem);
                    $this.focusedItem.addClass('ui-picklist-outline');
                    $this.ariaRegion.text($this.focusedItem.data('item-label'));
                }
            }, 100);
        })
        .on('blur.pickList', listSelector, null, function() {
            $this.removeOutline();
            $this.focusedItem = null;
        })
        .on('keydown.pickList', listSelector, null, function(e) {

            if(!$this.focusedItem) {
                return;
            }

            var list = $(this),
                keyCode = $.ui.keyCode,
                key = e.which;

            switch(key) {
                case keyCode.UP:
                    $this.removeOutline();

                    if(!$this.focusedItem.hasClass('ui-state-highlight')) {
                        $this.selectItem($this.focusedItem);
                    }
                    else {
                        var prevItem = $this.focusedItem.prevAll('.ui-picklist-item:visible:first');
                        if(prevItem.length) {
                            $this.unselectAll();
                            $this.selectItem(prevItem);
                            $this.focusedItem = prevItem;

                            PrimeFaces.scrollInView(list, $this.focusedItem);
                        }
                    }
                    $this.ariaRegion.text($this.focusedItem.data('item-label'));
                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    $this.removeOutline();

                    if(!$this.focusedItem.hasClass('ui-state-highlight')) {
                        $this.selectItem($this.focusedItem);
                    }
                    else {
                        var nextItem = $this.focusedItem.nextAll('.ui-picklist-item:visible:first');
                        if(nextItem.length) {
                            $this.unselectAll();
                            $this.selectItem(nextItem);
                            $this.focusedItem = nextItem;

                            PrimeFaces.scrollInView(list, $this.focusedItem);
                        }
                    }
                    $this.ariaRegion.text($this.focusedItem.data('item-label'));
                    e.preventDefault();
                break;

                case keyCode.ENTER:
                case keyCode.SPACE:
                    if($this.focusedItem && $this.focusedItem.hasClass('ui-state-highlight')) {
                        $this.focusedItem.trigger('dblclick.pickList');
                        $this.focusedItem = null;
                    }
                    e.preventDefault();
                break;
                default:
                    // #3304 find first item matching the character typed
                    var keyChar = String.fromCharCode(key).toLowerCase();
                    list.children('.ui-picklist-item').each(function() {
                        var item = $(this),
                            itemLabel = item.attr('data-item-label');
                        if (itemLabel.toLowerCase().indexOf(keyChar) === 0) {
                            $this.removeOutline();
                            $this.unselectAll();
                            $this.selectItem(item);
                            $this.focusedItem = item;
                            PrimeFaces.scrollInView(list, $this.focusedItem);
                            $this.ariaRegion.text($this.focusedItem.data('item-label'));
                            e.preventDefault();
                            return false;
                        }
                    });
                break;
            };
        });

    },

    /**
     * Removes the outline from the item that is currently focused.
     * @private
     */
    removeOutline: function() {
        if(this.focusedItem && this.focusedItem.hasClass('ui-picklist-outline')) {
            this.focusedItem.removeClass('ui-picklist-outline');
        }
    },

    /**
     * Select the given pick list item in the source or target list.
     * @param {JQuery} item A picklist item to select, with the class `ui-picklist-item`.
     * @param {boolean} [silent] `true` to imit triggering event listeners and behaviors, or `false` otherwise.
     */
    selectItem: function(item, silent) {
        item.addClass('ui-state-highlight');

        if(this.cfg.showCheckbox) {
            this.selectCheckbox(item.find('div.ui-chkbox-box'));
        }

        if(silent) {
            this.fireItemSelectEvent(item);
        }

        this.updateButtonsState();
    },

    /**
     * Unselect the given pick list item in the source or target list.
     * @param {JQuery} item A picklist item to unselect, with the class `ui-picklist-item`.
     * @param {boolean} [silent] `true` to imit triggering event listeners and behaviors, or `false` otherwise.
     */
    unselectItem: function(item, silent) {
        item.removeClass('ui-state-highlight');

        if(this.cfg.showCheckbox) {
            this.unselectCheckbox(item.find('div.ui-chkbox-box'));
        }

        if(silent) {
            this.fireItemUnselectEvent(item);
        }

        this.updateButtonsState();
    },

    /**
     * Unselects all items in the source and target list.
     */
    unselectAll: function() {
        var selectedItems = this.items.filter('.ui-state-highlight');
        for(var i = 0; i < selectedItems.length; i++) {
            this.unselectItem(selectedItems.eq(i));
        }
    },

    /**
     * Selects the given checkbox that belongs to a pick list item.
     * @private
     * @param {JQuery} chkbox The hidden checkbox of a pick list item that was selected.
     */
    selectCheckbox: function(chkbox) {
        chkbox.addClass('ui-state-active').children('span.ui-chkbox-icon').removeClass('ui-icon-blank').addClass('ui-icon-check');
    },

    /**
     * Unselects the given checkbox that belongs to a pick list item.
     * @private
     * @param {JQuery} chkbox The hidden checkbox of a pick list item that was unselected.
     */
    unselectCheckbox: function(chkbox) {
        chkbox.removeClass('ui-state-active').children('span.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check');
    },

    /**
     * Stores the current items in the given list in a hidden form field. Used for submitting the current value of this
     * pick list.
     * @private
     * @param {JQuery} list The source or target list with items to store.
     * @param {JQuery} input The hidden form field where the items are stored.
     */
    generateItems: function(list, input) {
        var $this = this;
        list.children('.ui-picklist-item').each(function() {
            var item = $(this),
            itemValue = item.attr('data-item-value'),
            itemLabel = item.attr('data-item-label') ? PrimeFaces.escapeHTML(item.attr('data-item-label')) : '',
            option = $('<option selected="selected"></option>');

            if ($this.cfg.escapeValue) {
               itemValue = PrimeFaces.escapeHTML(itemValue);
            }
            option.prop('value', itemValue).text(itemLabel);
            input.append(option);
        });
    },

    /**
     * Sets tup the event listeners for when the command buttons (move up, move down etc.) are pressed.
     * @private
     */
    bindButtonEvents: function() {
        var _self = this;

        //visuals
        PrimeFaces.skinButton(this.jq.find('.ui-button'));

        //events
        $(this.jqId + ' .ui-picklist-button-add').on("click", function() {_self.add();});
        $(this.jqId + ' .ui-picklist-button-add-all').on("click", function() {_self.addAll();});
        $(this.jqId + ' .ui-picklist-button-remove').on("click", function() {_self.remove();});
        $(this.jqId + ' .ui-picklist-button-remove-all').on("click", function() {_self.removeAll();});

        if(this.cfg.showSourceControls) {
            $(this.jqId + ' .ui-picklist-source-controls .ui-picklist-button-move-up').on("click", function() {_self.moveUp(_self.sourceList);});
            $(this.jqId + ' .ui-picklist-source-controls .ui-picklist-button-move-top').on("click", function() {_self.moveTop(_self.sourceList);});
            $(this.jqId + ' .ui-picklist-source-controls .ui-picklist-button-move-down').on("click", function() {_self.moveDown(_self.sourceList);});
            $(this.jqId + ' .ui-picklist-source-controls .ui-picklist-button-move-bottom').on("click", function() {_self.moveBottom(_self.sourceList);});
        }

        if(this.cfg.showTargetControls) {
            $(this.jqId + ' .ui-picklist-target-controls .ui-picklist-button-move-up').on("click", function() {_self.moveUp(_self.targetList);});
            $(this.jqId + ' .ui-picklist-target-controls .ui-picklist-button-move-top').on("click", function() {_self.moveTop(_self.targetList);});
            $(this.jqId + ' .ui-picklist-target-controls .ui-picklist-button-move-down').on("click", function() {_self.moveDown(_self.targetList);});
            $(this.jqId + ' .ui-picklist-target-controls .ui-picklist-button-move-bottom').on("click", function() {_self.moveBottom(_self.targetList);});
        }
    },

    /**
     * Sets up all event listeners for filtering the source and target lists.
     * @private
     */
    bindFilterEvents: function() {
        this.cfg.filterEvent = this.cfg.filterEvent||'keyup';
        this.cfg.filterDelay = this.cfg.filterDelay||300;
        this.setupFilterMatcher();

        this.sourceFilter = $(this.jqId + '_source_filter');
        this.targetFilter = $(this.jqId + '_target_filter');

        PrimeFaces.skinInput(this.sourceFilter);
        this.bindTextFilter(this.sourceFilter);

        PrimeFaces.skinInput(this.targetFilter);
        this.bindTextFilter(this.targetFilter);
    },

    /**
     * Sets up the event listeners for when text is entered into the filter input of the source or target list.
     * @private
     * @param {JQuery} filter The filter input of the source or target list.
     */
    bindTextFilter: function(filter) {
        if(this.cfg.filterEvent === 'enter')
            this.bindEnterKeyFilter(filter);
        else
            this.bindFilterEvent(filter);
    },

    /**
     * Sets up the event listeners for when the enter key is pressed while inside a filter input of the source or target
     * list.
     * @private
     * @param {JQuery} filter The filter input of the source or target list.
     */
    bindEnterKeyFilter: function(filter) {
        var $this = this;

        filter.on('keydown', PrimeFaces.utils.blockEnterKey)
        .on('keyup', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if((key === keyCode.ENTER)) {
                $this.filter(this.value, $this.getFilteredList($(this)));

                e.preventDefault();
            }
        });
    },

    /**
     * Sets up the event listeners for filtering the source and target lists.
     * @private
     * @param {JQuery} filter The filter input of the source or target list.
     */
    bindFilterEvent: function(filter) {
        var $this = this;

        //prevent form submit on enter key
        filter.on(this.cfg.filterEvent, function(e) {
            if (PrimeFaces.utils.ignoreFilterKey(e)) {
                return;
            }

            var input = $(this);

            if($this.filterTimeout) {
                clearTimeout($this.filterTimeout);
            }

            $this.filterTimeout = setTimeout(function() {
                $this.filter(input.val(), $this.getFilteredList(input));
                $this.filterTimeout = null;
            },
            $this.cfg.filterDelay);
        })
        .on('keydown', PrimeFaces.utils.blockEnterKey);
    },

    /**
     * Finds and stores the filter function which is to be used for filtering the options of this pick list.
     * @private
     */
    setupFilterMatcher: function() {
        this.cfg.filterMatchMode = this.cfg.filterMatchMode||'startsWith';
        this.filterMatchers = {
            'startsWith': this.startsWithFilter
            ,'contains': this.containsFilter
            ,'endsWith': this.endsWithFilter
            ,'custom': this.cfg.filterFunction
        };

        this.filterMatcher = this.filterMatchers[this.cfg.filterMatchMode];
    },

    /**
     * Filters the available options in the source or target list.
     * @param {string} value A value against which the available options are matched.
     * @param {JQuery} list The source or target list that is to be filtered.
     * @param {boolean} [animate] If it should be animated.
     */
    filter: function(value, list, animate) {
        var filterValue = PrimeFaces.trim(value).toLowerCase(),
        items = list.children('li.ui-picklist-item'),
        animated = animate || this.isAnimated();

        list.removeAttr('role');

        if(filterValue === '') {
            items.filter(':hidden').show();
            list.attr('role', 'menu');
        }
        else {
            for(var i = 0; i < items.length; i++) {
                var item = items.eq(i),
                itemLabel = item.attr('data-item-label'),
                matches = this.filterMatcher(itemLabel, filterValue);

                if(matches) {
                    var hasRole = list[0].hasAttribute('role');
                    if(animated) {
                        item.fadeIn('fast', function() {
                            if(!hasRole) {
                                list.attr('role', 'menu');
                            }
                        });
                    }
                    else {
                        item.show();
                        if(!hasRole) {
                            list.attr('role', 'menu');
                        }
                    }
                }
                else {
                    if(animated) {
                        item.fadeOut('fast');
                    }
                    else {
                        item.hide();
                    }
                }
            }
        }

    },

    /**
     * Implementation of a `PrimeFaces.widget.PickList.FilterFunction` that matches the given option when it starts with
     * the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options starts with the filter value, or `false` otherwise.
     */
    startsWithFilter: function(value, filter) {
        return value.toLowerCase().indexOf(filter) === 0;
    },

    /**
     * Implementation of a `PrimeFaces.widget.PickList.FilterFunction` that matches the given option when it contains
     * the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the contains the filter value, or `false` otherwise.
     */
    containsFilter: function(value, filter) {
        return value.toLowerCase().indexOf(filter) !== -1;
    },

    /**
     * Implementation of a `PrimeFaces.widget.PickList.FilterFunction` that matches the given option when it ends with
     * the given search text.
     * @param {string} value Text of an option.
     * @param {string} filter Value of the filter.
     * @return {boolean} `true` when the text of the options ends with the filter value, or `false` otherwise.
     */
    endsWithFilter: function(value, filter) {
        return value.indexOf(filter, value.length - filter.length) !== -1;
    },

    /**
     * Finds the list belonging to the given filter input.
     * @private
     * @param {JQuery} filter The filter input of either the target or source list.
     * @return {JQuery} The list to which the given filter input applies.
     */
    getFilteredList: function(filter) {
        return filter.hasClass('ui-source-filter-input') ? this.sourceList : this.targetList;
    },

    /**
     * Adds all selected items in the source list by transferring them to the target list.
     */
    add: function() {
        var items = this.sourceList.children('li.ui-picklist-item.ui-state-highlight')

        this.transfer(items, this.sourceList, this.targetList, 'command');
    },

    /**
     * Adds all items to the target list by transferring all items from the source list to the target list.
     */
    addAll: function() {
        var items = this.sourceList.children('li.ui-picklist-item:visible:not(.ui-state-disabled)');

        this.transfer(items, this.sourceList, this.targetList, 'command');
    },

    /**
     * Removes all selected items in the target list by transferring them to the source list.
     */
    remove: function() {
        var items = this.targetList.children('li.ui-picklist-item.ui-state-highlight');

        this.transfer(items, this.targetList, this.sourceList, 'command');
    },

    /**
     * Removes all items in the target list by transferring all items from the target list to the source list.
     */
    removeAll: function() {
        var items = this.targetList.children('li.ui-picklist-item:visible:not(.ui-state-disabled)');

        this.transfer(items, this.targetList, this.sourceList, 'command');
    },

    /**
     * Moves the items that are currently selected up by one.
     * @param {JQuery} list The source or target list with items to move up.
     */
    moveUp: function(list) {
        var _self = this,
        animated = _self.isAnimated(),
        items = list.children('.ui-state-highlight'),
        itemsCount = items.length,
        movedCount = 0;

        if(itemsCount) {
            items.each(function() {
                var item = $(this);

                if(!item.is(':first-child')) {

                    if(animated) {
                        item.hide(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                            item.insertBefore(item.prev()).show(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                                movedCount++;

                                if(movedCount === itemsCount) {
                                    _self.saveState();
                                    _self.fireReorderEvent();
                                }
                            });
                        });
                    }
                    else {
                        item.hide().insertBefore(item.prev()).show();
                    }

                }
            });

            if(!animated) {
                this.saveState();
                this.fireReorderEvent();
            }
        }

    },

    /**
     * Moves the items that are currently selected to the top of the source of target list.
     * @param {JQuery} list The source or target list with items to move to the top.
     */
    moveTop: function(list) {
        var _self = this,
        animated = _self.isAnimated(),
        items = list.children('.ui-state-highlight'),
        itemsCount = items.length,
        movedCount = 0;

        if(itemsCount) {
            items.each(function() {
                var item = $(this);

                if(!item.is(':first-child')) {

                    if(animated) {
                        item.hide(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                            item.prependTo(item.parent()).show(_self.cfg.effect, {}, _self.cfg.effectSpeed, function(){
                                movedCount++;

                                if(movedCount === itemsCount) {
                                    _self.saveState();
                                    _self.fireReorderEvent();
                                }
                            });
                        });
                    }
                    else {
                        item.hide().prependTo(item.parent()).show();
                    }
                }
            });

            if(!animated) {
                this.saveState();
                this.fireReorderEvent();
            }
        }
    },

    /**
     * Moves the items that are currently selected down by one.
     * @param {JQuery} list The source or target list with items to move down.
     */
    moveDown: function(list) {
        var _self = this,
        animated = _self.isAnimated(),
        items = list.children('.ui-state-highlight'),
        itemsCount = items.length,
        movedCount = 0;

        if(itemsCount) {
            $(items.get().reverse()).each(function() {
                var item = $(this);

                if(!item.is(':last-child')) {
                    if(animated) {
                        item.hide(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                            item.insertAfter(item.next()).show(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                                movedCount++;

                                if(movedCount === itemsCount) {
                                    _self.saveState();
                                    _self.fireReorderEvent();
                                }
                            });
                        });
                    }
                    else {
                        item.hide().insertAfter(item.next()).show();
                    }
                }

            });

            if(!animated) {
                this.saveState();
                this.fireReorderEvent();
            }
        }
    },

    /**
     * Moves the items that are currently selected to the bottom of the source of target list.
     * @param {JQuery} list The source or target list with items to move to the bottom.
     */
    moveBottom: function(list) {
        var _self = this,
        animated = _self.isAnimated(),
        items = list.children('.ui-state-highlight'),
        itemsCount = items.length,
        movedCount = 0;

        if(itemsCount) {
            items.each(function() {
                var item = $(this);

                if(!item.is(':last-child')) {

                    if(animated) {
                        item.hide(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                            item.appendTo(item.parent()).show(_self.cfg.effect, {}, _self.cfg.effectSpeed, function() {
                                movedCount++;

                                if(movedCount === itemsCount) {
                                    _self.saveState();
                                    _self.fireReorderEvent();
                                }
                            });
                        });
                    }
                    else {
                        item.hide().appendTo(item.parent()).show();
                    }
                }

            });

            if(!animated) {
                this.saveState();
                this.fireReorderEvent();
            }
        }
    },

    /**
     * Saves the current state of this widget, i.e. to which list the items are currently assigned. Clears inputs and
     * repopulates them from the list states.
     * @private
     */
    saveState: function() {
        this.sourceInput.children().remove();
        this.targetInput.children().remove();

        this.generateItems(this.sourceList, this.sourceInput);
        this.generateItems(this.targetList, this.targetInput);
        this.cursorItem = null;
    },

    /**
     * Transfers the given items from the source or target list to the other list.
     * @param {JQuery} items Items that were transferred from one list to the other.
     * @param {JQuery} from List from which the items were transferred.
     * @param {JQuery} to List to which the items were transferred.
     * @param {PrimeFaces.widget.PickList.TransferType} type Type of the action that caused the items to be transferred.
     */
    transfer: function(items, from, to, type) {
        $(this.jqId + ' ul').sortable('disable');
        var $this = this;
        var itemsCount = items.length;
        var transferCount = 0;

        if(this.isAnimated()) {
            items.hide(this.cfg.effect, {}, this.cfg.effectSpeed, function() {
                var item = $(this);
                $this.unselectItem(item);

                item.appendTo(to).show($this.cfg.effect, {}, $this.cfg.effectSpeed, function() {
                    transferCount++;

                    //fire transfer when all items are transferred
                    if(transferCount == itemsCount) {
                        $this.saveState();
                        $this.fireTransferEvent(items, from, to, type);
                    }
                });

                $this.updateListRole();
            });
        }
        else {
            items.hide();

            if(this.cfg.showCheckbox) {
                items.each(function() {
                    $this.unselectItem($(this));
                });
            }

            items.appendTo(to).show();

            this.saveState();
            this.fireTransferEvent(items, from, to, type);
            this.updateListRole();
        }
    },

    /**
     * Triggers the behavior for when pick list items are transferred from the source to the target list or vice-versa.
     * @private
     * @param {JQuery} items Items that were transferred from one list to the other.
     * @param {JQuery} from List from which the items were transferred.
     * @param {JQuery} to List to which the items were transferred.
     * @param {PrimeFaces.widget.PickList.TransferType} type Type of the action that caused the items to be transferred.
     */
    fireTransferEvent: function(items, from, to, type) {
        var $this = this;

        if(this.cfg.onTransfer) {
            var obj = {};
            obj.items = items;
            obj.from = from;
            obj.to = to;
            obj.type = type;

            this.cfg.onTransfer.call(this, obj);
        }

        if(this.hasBehavior('transfer')) {
            var isAdd = from.hasClass('ui-picklist-source');

            var options = {
                params: [
                    {name: $this.id + '_add', value: isAdd}
                ],
                oncomplete: function() {
                    $this.refilterSource();
                    $this.refilterTarget();
                    $($this.jqId + ' ul').sortable('enable');
                    $this.updateButtonsState();
                }
            };

            items.each(function(index, item) {
                options.params.push({name: $this.id + '_transferred', value: $(item).attr('data-item-value')});
            });

            this.callBehavior('transfer', options);
        }
        else {
            $($this.jqId + ' ul').sortable('enable');
            $this.updateButtonsState();
        }
    },

    /**
     * Finds the type of the given list, i.e. whether the list represents the source or target list.
     * @private
     * @param {JQuery} element A list element to check.
     * @return {PrimeFaces.widget.PickList.ListName} Whether the element represents the source or target list.
     */
    getListName: function(element){
        return element.parent().hasClass("ui-picklist-source") ? "source" : "target";
    },

    /**
     * Triggers the behavior for when pick list items are selected.
     * @private
     * @param {JQuery} item A pick list item that was selected.
     */
    fireItemSelectEvent: function(item) {
        if(this.hasBehavior('select')) {
            var listName = this.getListName(item),
            inputContainer = (listName === "source") ? this.sourceInput : this.targetInput,
            ext = {
                params: [
                    {name: this.id + '_itemIndex', value: item.index()},
                    {name: this.id + '_listName', value: listName}
                ],
                onstart: function() {
                    if(!inputContainer.children().length) {
                        return false;
                    }
                }
            };

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

    /**
     * Triggers the behavior for when pick list items are unselected.
     * @private
     * @param {JQuery} item A pick list item that was unselected.
     */
    fireItemUnselectEvent: function(item) {
        if(this.hasBehavior('unselect')) {
            var ext = {
                params: [
                    {name: this.id + '_itemIndex', value: item.index()},
                    {name: this.id + '_listName', value: this.getListName(item)}
                ]
            };

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

    /**
     * Triggers the behavior for when pick list items are reordered.
     * @private
     */
    fireReorderEvent: function() {
        this.callBehavior('reorder');
    },

    /**
     * Checks whether UI actions of this pick list are animated.
     * @return {boolean} `true` if this pick list is animated, or `false` otherwise.
     */
    isAnimated: function() {
        return (this.cfg.effect && this.cfg.effect != 'none');
    },

    /**
     * Applies the tab index to this pick list widget.
     * @private
     */
    setTabIndex: function() {
        var tabindex = (this.cfg.disabled) ? '-1' : this.getTabIndex();
        this.sourceList.attr('tabindex', tabindex);
        this.targetList.attr('tabindex', tabindex);
        $(this.jqId + ' button').attr('tabindex', tabindex);
        $(this.jqId + ' .ui-picklist-filter-container > input').attr('tabindex', tabindex);
    },

    /**
     * Finds the tab index of this pick list widget.
     * @private
     * @return {string} The tab index of this pick list.
     */
    getTabIndex: function() {
        return this.cfg.tabindex||'0';
    },

    /**
     * Updates the state of all buttons of this pick list, such as whether they are disabled or enabled.
     * @private
     */
    updateButtonsState: function () {
        var addButton = $(this.jqId + ' .ui-picklist-button-add');
        var sourceListButtons = $(this.jqId + ' .ui-picklist-source-controls .ui-button');
        if (this.sourceList.find('li.ui-state-highlight').length) {
            this.enableButton(addButton);
            this.enableButton(sourceListButtons);
        }
        else {
            this.disableButton(addButton);
            this.disableButton(sourceListButtons);
        }

        var removeButton = $(this.jqId + ' .ui-picklist-button-remove');
        var targetListButtons = $(this.jqId + ' .ui-picklist-target-controls .ui-button');
        if (this.targetList.find('li.ui-state-highlight').length) {
            this.enableButton(removeButton);
            this.enableButton(targetListButtons);
        }
        else {
            this.disableButton(removeButton);
            this.disableButton(targetListButtons);
        }

        var addAllButton = $(this.jqId + ' .ui-picklist-button-add-all');
        if (this.sourceList.find('li.ui-picklist-item:not(.ui-state-disabled)').length) {
            this.enableButton(addAllButton);
            this.sourceList.attr('tabindex', this.getTabIndex());
        }
        else {
            this.disableButton(addAllButton);
            this.sourceList.attr('tabindex', '-1');
        }

        var removeAllButton = $(this.jqId + ' .ui-picklist-button-remove-all');
        if (this.targetList.find('li.ui-picklist-item:not(.ui-state-disabled)').length) {
            this.enableButton(removeAllButton);
            this.targetList.attr('tabindex', this.getTabIndex());
        }
        else {
            this.disableButton(removeAllButton);
            this.targetList.attr('tabindex', '-1');
        }
    },

    /**
     * Reapply filtering the current source list.
     * @private
     */
    refilterSource: function() {
        this.filter(this.sourceFilter.val(), this.sourceList, false);
    },

    /**
     * Reapply filtering to the current target list.
     * @private
     */
    refilterTarget: function() {
        this.filter(this.targetFilter.val(), this.targetList, false);
    },

    /**
     * Disables the given button belonging to this pick list.
     * @private
     * @param {JQuery} button A button to disable.
     */
    disableButton: function (button) {
        if (button.hasClass('ui-state-focus')) {
            button.trigger("blur");
        }

        button.attr('disabled', 'disabled').addClass('ui-state-disabled');
        button.attr('tabindex', '-1');
    },

    /**
     * Enables the given button belonging to this pick list.
     * @private
     * @param {JQuery} button A button to enable.
     */
    enableButton: function (button) {
        button.removeAttr('disabled').removeClass('ui-state-disabled');
        button.attr('tabindex', this.getTabIndex());
    },

    /**
     * Updates the `role` attribute of the source and target pick list items.
     * @private
     */
    updateListRole: function() {
        this.sourceList.children('li:visible').length > 0 ? this.sourceList.attr('role', 'menu') : this.sourceList.removeAttr('role');
        this.targetList.children('li:visible').length > 0 ? this.targetList.attr('role', 'menu') : this.targetList.removeAttr('role');
    }

});
/**
 * __PrimeFaces ProgressBar widget__
 * 
 * ProgressBar is a process status indicator that can either work purely on client side or interact with server side
 * using AJAX.
 * 
 * @prop {JQuery} jqLabel The DOM element for the label of the progress bar.
 * @prop {JQuery} jqValue The DOM element for the value of the progress bar.
 * @prop {number} progressPoll The set-timeout timer ID of the time used for polling when `ajax` is set to `true`.
 * @prop {number} value The current value of this progress bar.
 * 
 * @interface {PrimeFaces.widget.ProgressBarCfg} cfg The configuration for the {@link  ProgressBar| ProgressBar 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.ajax Specifies the mode of the progress bar, in AJAX mode progress value is retrieved from a
 * backing bean.
 * @prop {number} cfg.animationDuration Animation duration in milliseconds determining how long the animation will run.
 * @prop {string} cfg.formId ID of the form used for AJAX requests.
 * @prop {boolean} cfg.global Global AJAX requests are listened to by the `ajaxStatus` component, setting global to
 * `false` will not trigger `ajaxStatus`.
 * @prop {number} cfg.initialValue The initial value for the progress bar.
 * @prop {number} cfg.interval Duration in milliseconds between two AJAX polling requests, when `ajax` is set to `true`.
 * @prop {string} cfg.labelTemplate Template of the progress label.
 */
PrimeFaces.widget.ProgressBar = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.jqValue = this.jq.children('.ui-progressbar-value');
        this.jqLabel = this.jq.children('.ui-progressbar-label');
        this.value = this.cfg.initialValue;
        this.cfg.global = (this.cfg.global === false) ? false : true;

        this.enableARIA();
    },

    /**
     * Sets the value (progress) of this progress bar to a value between zero and a hundred percent.
     * @param {number} value New value for this progress bar, between `0` and `100`.
     */
    setValue: function(value) {
        if(value >= 0 && value<=100) {
            if(value == 0) {
                this.jqValue.hide().css('width', '0%').removeClass('ui-corner-right');

                this.jqLabel.hide();
            }
            else {
                this.jqValue.show().animate({
                    'width': value + '%'
                }, this.cfg.animationDuration, 'easeInOutCirc');

                if(this.cfg.labelTemplate) {
                    var formattedLabel = this.cfg.labelTemplate.replace(/{value}/gi, value);
                    this.jqLabel.text(formattedLabel).show();
                }
            }

            this.value = value;
            this.jq.attr('aria-valuenow', value);
        }
    },

    /**
     * Finds the progress currently shown by this progress bar.
     * @return {number} The current value of this progress bar, between `0` and `100`.
     */
    getValue: function() {
        return this.value;
    },

    /**
     * Starts the progress bar, if not already started. Does not reset its current value.
     */
    start: function() {
        var $this = this;

        if(this.cfg.ajax) {

            this.progressPoll = setInterval(function() {
                var options = {
                    source: $this.id,
                    process: $this.id,
                    formId: $this.getParentFormId(),
                    global: $this.cfg.global,
                    async: true,
                    oncomplete: function(xhr, status, args, data) {
                        var value = args[$this.id + '_value'];
                        $this.setValue(value);

                        //trigger complete listener
                        if(value === 100) {
                            $this.fireCompleteEvent();
                        }
                    }
                };

                PrimeFaces.ajax.Request.handle(options);

            }, this.cfg.interval);
        }
    },

    /**
     * Invokes the behavior for when the progress bar is complete.
     * @private
     */
    fireCompleteEvent: function() {
        clearInterval(this.progressPoll);

        this.callBehavior('complete');
    },

    /**
     * Cancels the progress bar, resetting it back to zero percent.
     */
    cancel: function() {
        clearInterval(this.progressPoll);
        this.setValue(0);
    },

    /**
     * Adds the appropriate aria attributes.
     * @private
     */
    enableARIA: function() {
        this.jq.attr('role', 'progressbar')
                .attr('aria-valuemin', 0)
                .attr('aria-valuenow', this.value)
                .attr('aria-valuemax', 100);
    }

});
/**
 * __PrimeFaces Rating Widget__
 * 
 * Rating component features a star based rating system.
 * 
 * @prop {JQuery} cancel The DOM element for the cancel button.
 * @prop {JQuery} jqInput The DOM element for the hidden input field storing the value of this widget.
 * @prop {JQuery} stars The DOM elements for the clickable stars.
 * @prop {number} value The current value, i.e. the number of selected stars
 * 
 * @typedef PrimeFaces.widget.Rating.OnRateCallback Callback that is invoked when the user gives a rating. See also
 * {@link RatingCfg.onRate}.
 * @this {PrimeFaces.widget.Rating} PrimeFaces.widget.Rating.OnRateCallback 
 * @param {number} PrimeFaces.widget.Rating.OnRateCallback.currentNumberOfStars The number of rated stars.
 * 
 * @interface {PrimeFaces.widget.RatingCfg} cfg The configuration for the {@link  Rating| Rating 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.disabled Whether this widget is initially disabled.
 * @prop {PrimeFaces.widget.Rating.OnRateCallback} cfg.onRate Callback that is invoked when the user gives a rating.
 * @prop {boolean} cfg.readonly Whether this widget is in read-only mode.
 */
PrimeFaces.widget.Rating = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.jqInput = $(this.jqId + '_input');
        this.value = this.getValue();
        this.stars = this.jq.children('.ui-rating-star');
        this.cancel = this.jq.children('.ui-rating-cancel');

        if(!this.cfg.disabled && !this.cfg.readonly) {
            this.bindEvents();
        }

        if(this.cfg.readonly) {
            this.jq.children().css('cursor', 'default');
        }
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.stars.on("click", function() {
            var value = $this.stars.index(this) + 1;   //index starts from zero

            $this.setValue(value);
        });

        this.cancel.on("mouseenter", function() {
             $(this).addClass('ui-rating-cancel-hover');
        }).on("mouseleave", function() {
             $(this).removeClass('ui-rating-cancel-hover');
        }).on("click", function() {
            $this.reset();
        });
    },

    /**
     * Removes the event listeners that were added, called when this widget is disabled.
     * @private
     */
    unbindEvents: function() {
        this.stars.off('click');

        this.cancel.off('hover click');
    },

    /**
     * Finds the current rating, i.e. the number of stars selected.
     * @return {number | null} The current rating value.
     */
    getValue: function() {
        var inputVal = this.jqInput.val();

        return inputVal == '' ? null : parseInt(inputVal);
    },

    /**
     * Sets the rating to the given value.
     * @param {number | undefined | null} value New rating value to set (number of stars selected). Pass `undefined` or
     * a value not greater thatn 0 to reset the value.
     */
    setValue: function(value) {
        if(this.isDisabled() || this.isReadOnly()) {
            return;
        }

        // check minimum and maximum
        var newValue = parseInt(value);
        if(isNaN(newValue) || newValue <= 0) {
            this.reset();
            return;
        }
        else if(newValue > this.stars.length) {
            newValue = this.stars.length;
        }

        //set hidden value
        this.jqInput.val(newValue);

        //update visuals
        this.stars.removeClass('ui-rating-star-on');
        for(var i = 0; i < newValue; i++) {
            this.stars.eq(i).addClass('ui-rating-star-on');
        }

        //invoke callback
        if(this.cfg.onRate) {
            this.cfg.onRate.call(this, newValue);
        }

        this.callBehavior('rate');
    },
    
    /**
     * Checks whether this widget is currently disabled. Whe disabled, the user cannot edit the value and it will not be
     * sent to the server when the form is submitted.
     * @return {boolean} `true` if this rating widget is disabled, `false` otherwise.
     */
    isDisabled: function() {
        return this.jq.hasClass('ui-state-disabled');
    },
    
    /**
     * Checks whether this widget is currently read-only. When read-only, the user cannot edit the value, but the value
     * will be sent to the server when the form is submitted.
     * @return {boolean} `true` if this rating widget is read-only, `false` otherwise.
     */
    isReadOnly: function() {
        return this.jqInput.is('[readonly]');
    },

    /**
     * Enables this rating widget so the user can give a rating.
     */
    enable: function() {
        if(!this.isDisabled() || this.isReadOnly()) {
           return; 
        }
        this.cfg.disabled = false;

        this.bindEvents();

        this.jq.removeClass('ui-state-disabled');
    },

    /**
     * Disables this rating widget so the user cannot give a rating anymore.
     */
    disable: function() {
        if(this.isDisabled()) {
           return; 
        }
        this.cfg.disabled = true;

        this.unbindEvents();

        this.jq.addClass('ui-state-disabled');
    },

    /**
     * Resets the rating so that no stars are selected.
     */
    reset: function() {
        this.jqInput.val('');

        this.stars.filter('.ui-rating-star-on').removeClass('ui-rating-star-on');

        this.callBehavior('cancel');
    }
});
/**
 * __PrimeFaces Resizable Widget__
 * 
 * Resizable component is used to make another JSF component resizable.
 * 
 * @typedef PrimeFaces.widget.Resizable.OnResizeCallback Client-side callback to execute during resizing. See also
 * {@link ResizableCfg.onResize}.
 * @this {PrimeFaces.widget.Resizable} PrimeFaces.widget.Resizable.OnResizeCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Resizable.OnResizeCallback.event The event that triggered the resize.
 * @param {JQueryUI.ResizableUIParams} PrimeFaces.widget.Resizable.OnResizeCallback.ui The details about the resize.
 * 
 * @typedef PrimeFaces.widget.Resizable.OnStartCallback Client-side callback to execute when resizing begins. See also
 * {@link ResizableCfg.onStart}.
 * @this {PrimeFaces.widget.Resizable} PrimeFaces.widget.Resizable.OnStartCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Resizable.OnStartCallback.event The event that triggered the resizing to
 * start.
 * @param {JQueryUI.ResizableUIParams} PrimeFaces.widget.Resizable.OnStartCallback.ui Details about the resize.
 * 
 * @typedef PrimeFaces.widget.Resizable.OnStopCallback Client-side callback to execute after resizing end. See also
 * {@link ResizableCfg.onStop}.
 * @this {PrimeFaces.widget.Resizable} PrimeFaces.widget.Resizable.OnStopCallback 
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Resizable.OnStopCallback.event The event that triggered the resize to end.
 * @param {JQueryUI.ResizableUIParams} PrimeFaces.widget.Resizable.OnStopCallback.ui Details about the resize.
 * 
 * @prop {JQuery} jqTarget The DOM element for the target widget t o be resized.
 * 
 * @interface {PrimeFaces.widget.ResizableCfg} cfg The configuration for the {@link  Resizable| Resizable 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.ajaxResize Whether AJAX requests are sent when the element is resized.
 * @prop {string} cfg.containment ID of the element to which the target widget is constrained.
 * @prop {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {PrimeFaces.widget.Resizable.OnResizeCallback} cfg.onResize Client-side callback to execute during resizing.
 * @prop {PrimeFaces.widget.Resizable.OnStartCallback} cfg.onStart Client-side callback to execute when resizing
 * begins.
 * @prop {PrimeFaces.widget.Resizable.OnStopCallback} cfg.onStop Client-side callback to execute after resizing end.
 * @prop {string} cfg.parentComponentId ID of the parent of the resizable element. 
 * @prop {JQueryUI.ResizableEvent} cfg.resize Callback passed to JQuery UI for when a resizing event occurs.
 * @prop {JQueryUI.ResizableEvent} cfg.start Callback passed to JQuery UI for when a resizing event starts.
 * @prop {JQueryUI.ResizableEvent} cfg.stop Callback passed to JQuery UI for when a resizing event ends.
 * @prop {string} cfg.target ID of the target widget or element to be resized. 
 */
PrimeFaces.widget.Resizable = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.jqTarget = $(PrimeFaces.escapeClientId(this.cfg.target));

        this.renderDeferred();
    },

    /**
     * Renders this widget, if the target widget is already visible, or adds a deferred renderer otherwise.
     * @private
     */
    renderDeferred: function() {
        if(this.jqTarget.is(':visible')) {
            this._render();
        }
        else {
            var container = this.jqTarget.parent()[0].closest('.ui-hidden-container');
            if (container) {
                var $container = $(container);
                if ($container.length) {
                    var $this = this;
                    PrimeFaces.addDeferredRender(this.id, $container.attr('id'), function() {
                        return $this.render();
                    });
                }
            }
        }
    },

    /**
     * Renders the client-side parts of this widget, if this target widget to be resized is already visible.
     * @private
     * @return {boolean} `true` if the target widget is visible, or `false` otherwise.
     */
    render: function() {
        if(this.jqTarget.is(':visible')) {
            this._render();
            return true;
        }

        return false;
    },

    /**
     * Renders the client-side parts of this widget.
     * @private
     */
    _render: function() {
        if(this.cfg.ajaxResize) {
            this.cfg.formId = $(this.target).parents('form:first').attr('id');
        }

        if (this.cfg.isContainment) {
        	this.cfg.containment = PrimeFaces.escapeClientId(this.cfg.parentComponentId);
        }

        var _self = this;

        this.cfg.stop = function(event, ui) {
            if(_self.cfg.onStop) {
                _self.cfg.onStop.call(_self, event, ui);
            }

            _self.fireAjaxResizeEvent(event, ui);
        };

        this.cfg.start = function(event, ui) {
            if(_self.cfg.onStart) {
                _self.cfg.onStart.call(_self, event, ui);
            }
        };

        this.cfg.resize = function(event, ui) {
            if(_self.cfg.onResize) {
                _self.cfg.onResize.call(_self, event, ui);
            }
        };

        this.jqTarget.resizable(this.cfg);

        this.removeScriptElement(this.id);
    },

    /**
     * Triggers the behavior for when the component was resized.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that triggered the resize.
     * @param {JQueryUI.ResizableUIParams} ui Data of the resize event.
     */
    fireAjaxResizeEvent: function(event, ui) {
        if(this.hasBehavior('resize')) {
            var ext = {
                params: [
                    {name: this.id + '_width', value: parseInt(ui.helper.width())},
                    {name: this.id + '_height', value: parseInt(ui.helper.height())}
                ]
            };

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

});
/**
 * __PrimeFaces Slider Widget__
 * 
 * 
 * Slider is used to provide input with various customization options like orientation, display modes and skinning.
 *
 * @typedef PrimeFaces.widget.Slider.SliderCallback
 * A callback function that is invoked when a slider handle is moved or starts or ends moving.
 * @this {PrimeFaces.widget.Slider} PrimeFaces.widget.Slider.SliderCallback
 * @param {JQuery.TriggeredEvent} PrimeFaces.widget.Slider.SliderCallback.event The event that triggered the slider event, as
 * given by jQuery.
 * @param {JQueryUI.SliderUIParams} PrimeFaces.widget.Slider.SliderCallback.ui Details about the slider, as given by
 * the jQueryUI slider widget.
 * 
 * @prop {boolean} decimalStep `true` if the  {@link SliderCfg.step} has a fractional part, or `false` it is is an
 * integer.
 * @prop {JQuery} input The DOM elements for the hidden input fields storing the value of each slider handle.
 * @prop {JQuery} output The DOM element displaying the current value of the slider.
 *  
 * @interface {PrimeFaces.widget.SliderCfg} cfg The configuration for the {@link  Slider| Slider 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
 * @extends {JQueryUI.SliderOptions} cfg
 * 
 * @prop {string} cfg.display ID of the component to display the slider value.
 * @prop {string} cfg.displayTemplate String template to use when updating the display. Valid placeholders are
 * `{value}`, `{min}` and `{max}`.
 * @prop {string} cfg.input IDs of the hidden {@link Slider.input} fields storing the values of each slider handle,
 * separated by a comma.
 * @prop {PrimeFaces.widget.Slider.SliderCallback} cfg.onSlide Client side callback that is invoked when a slider handle
 * is moved.
 * @prop {PrimeFaces.widget.Slider.SliderCallback} cfg.onSlideEnd Client side callback that is invoked when a slider
 * handle stops moving.
 * @prop {PrimeFaces.widget.Slider.SliderCallback} cfg.onSlideStart Client side callback that is invoked when a slider
 * handle starts moving.
 */
PrimeFaces.widget.Slider = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.displayTemplate = this.cfg.displayTemplate||(this.cfg.range === true ? '{min} - {max}' : '{value}');

        if(this.cfg.range === true) {
            var inputIds = this.cfg.input.split(',');
            this.input = $(PrimeFaces.escapeClientId(inputIds[0]) + ',' + PrimeFaces.escapeClientId(inputIds[1]));
        }
        else {
            this.input = $(PrimeFaces.escapeClientId(this.cfg.input));
        }

        if(this.cfg.display) {
            this.output = $(PrimeFaces.escapeClientId(this.cfg.display));
        }

        this.jq.slider(this.cfg);

        this.decimalStep = !(this.cfg.step % 1 === 0);

        this.bindEvents();

        if (PrimeFaces.env.isTouchable(this.cfg)) {
            this.bindTouchEvents();
        }
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.jq.on('slide', function(event, ui) {
            $this.onSlide(event, ui);
        });

        if(this.cfg.onSlideStart) {
            this.jq.on('slidestart', function(event, ui) {
                $this.cfg.onSlideStart.call(this, event, ui);
            });
        }

        this.jq.on('slidestop', function(event, ui) {
            $this.onSlideEnd(event, ui);
        });

        if (this.input.parent().hasClass('ui-inputnumber')) {
            this.input.parent().find('input:hidden').off('change.slider').on('change.slider', function () {
                $this.setValue($(this).val());
            });
        }
        else {
            this.input.on('keydown.slider', function (e) {
                var keyCode = $.ui.keyCode,
                key = e.which;

                switch(key) {
                    case keyCode.UP:
                    case keyCode.DOWN:
                    case keyCode.LEFT:
                    case keyCode.RIGHT:
                    case keyCode.BACKSPACE:
                    case keyCode.DELETE:
                    case keyCode.END:
                    case keyCode.HOME:
                    case keyCode.TAB:
                    break;

                    default:
                        if (key < 32) return true; // Control chars (charcode)
                        var character = e.key;
                        var current = $(this).val();

                         // don't allow duplicate decimal separators
                        var separatorRegex ='';
                        if($this.decimalStep) {
                            if (character === ',') {
                                if(current.indexOf(',') !== -1) {
                                    return false;
                                }
                                else {
                                    separatorRegex = ',';
                                }
                            } 
                            if (character === '.') {
                                if(current.indexOf('.') !== -1) {
                                    return false;
                                }
                                else {
                                    separatorRegex = '\\.';
                                }
                            } 
                        }

                        // #6319 only allow negative once and if min < 0
                        var negativeRegex = '';
                        if ($this.cfg.min < 0) {
                            if (character === '-' && current.indexOf('-') !== -1) return false;
                            negativeRegex = '-';
                        }
                        var regex = new RegExp('[^0-9' + separatorRegex + negativeRegex + ']', 'g');
                        return !character.match(regex);
                    break;
                }
            }).on('keyup.slider', function (e) {
                $this.setValue($this.input.val());
            });
        }
    },

    /**
     * Sets up all touch and mouse related event listeners that are required by this widget.
     * @private
     */
    bindTouchEvents: function() {
        var eventMapping = {
            touchstart: 'mousedown',
            touchmove: 'mousemove',
            touchend: 'mouseup'
        };

        this.jq.children('.ui-slider-handle').on('touchstart touchmove touchend', function(e) {
            var touch = e.originalEvent.changedTouches[0];
            var targetEvent = document.createEvent('MouseEvent');

            targetEvent.initMouseEvent(
                    eventMapping[e.originalEvent.type],
                    true, // canBubble
                    true, // cancelable
                    window, // view
                    1, // detail
                    touch.screenX,
                    touch.screenY,
                    touch.clientX,
                    touch.clientY,
                    false, // ctrlKey
                    false, // altKey
                    false, // shiftKey
                    false, // metaKey
                    0, // button
                    null // relatedTarget
                );

            touch.target.dispatchEvent(targetEvent);
            e.preventDefault();
        });
    },

    /**
     * Callback that is invoked when the user moves a slider handle.
     * @private
     * @param {JQuery.TriggeredEvent} event The event that triggered the slider handle to move.
     * @param {JQueryUI.SliderUIParams} ui Details about the slider.
     */
    onSlide: function(event, ui) {
        if(this.cfg.onSlide) {
            this.cfg.onSlide.call(this, event, ui);
        }

        if(this.cfg.range === true) {
            this.setInputValue(this.input.eq(0), ui.values[0]);
            this.setInputValue(this.input.eq(1), ui.values[1]);

            if(this.output) {
                this.output.text(this.cfg.displayTemplate.replace('{min}', ui.values[0]).replace('{max}', ui.values[1]));
            }
        }
        else {
            this.setInputValue(this.input, ui.value);

            if(this.output) {
                this.output.text(this.cfg.displayTemplate.replace('{value}', ui.value));
            }
        }
    },

    /**
     * Stores the given slider handle value in the given hidden input field.
     * @private
     * @param {JQuery} input A hidden input field that should store the value.
     * @param {number} inputValue A value of a slider handle to store.
     */
    setInputValue: function(input, inputValue) {
        if (input.parent().hasClass('ui-inputnumber')) {
            var inputNumberId = input.closest('.ui-inputnumber').attr('id');
            var inputNumberWidget = PrimeFaces.getWidgetById(inputNumberId);
            inputNumberWidget.autonumeric.set(inputValue);
        }
        else if (input.hasClass('ui-spinner-input')) {
            var spinnerId = input.closest('.ui-spinner').attr('id');
            var spinnerWidget = PrimeFaces.getWidgetById(spinnerId);
            spinnerWidget.setValue(inputValue);
        }
        else {
            input.val(inputValue);
        }
    },

    /**
     * Triggers the change event on the hidden input.
     * @private
     * @param {JQuery} input The slider input element.
     */
    triggerOnchange: function(input) {
        if (input.parent().hasClass('ui-inputnumber')) {
            input.trigger('change');
        }
        else if (input.hasClass('ui-spinner-input')) {
            input.trigger('change');
        }
    },

    /**
     * Callback that is invoked when the user is done moving a slider handle.
     * @private
     * @param {JQuery.TriggeredEvent} event The event that triggered the slide to end.
     * @param {JQueryUI.SliderUIParams} ui Details about the slider.
     */
    onSlideEnd: function(event, ui) {
        if(this.cfg.onSlideEnd) {
            this.cfg.onSlideEnd.call(this, event, ui);
        }

        if(this.cfg.range === true) {
            this.triggerOnchange(this.input.eq(0));
            this.triggerOnchange(this.input.eq(1));
        }
        else {
            this.triggerOnchange(this.input);
        }

        if(this.hasBehavior('slideEnd')) {
            var ext = {
                params: [
                    {name: this.id + '_slideValue', value: ui.value}
                ]
            };

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

    /**
     * Determines the value of the slider, if there is only one handle. If there is more than one handle, determines the
     * value of the first handle.
     * @return {number} value The value of the first slider handler. 
     */
    getValue: function() {
        return this.jq.slider('value');
    },

    /**
     * Sets the value of the slider, if there is only one handle. If there is more than one handle, sets the
     * value of the first handle.
     * @param {number} value The value for the first slider handler. 
     */
    setValue: function(value) {
        this.jq.slider('value', value);
    },

    /**
     * Finds the values of all slider handles.
     * @return {number[]} A list with the values of all handles.
     */
    getValues: function() {
        return this.jq.slider('values');
    },

    /**
     * Sets the values of all slider handlers.
     * @param {number[]} values The new values for the handles.
     */
    setValues: function(values) {
        this.jq.slider('values', values);
    },

    /**
     * Enables this slider widget so that the user can move the slider.
     */
    enable: function() {
        this.jq.slider('enable');
    },

    /**
     * Disables this slider widget so that the user cannot move the slider anymore.
     */
    disable: function() {
        this.jq.slider('disable');
    }
});
/**
 * __PrimeFaces Spinner Widget__
 * 
 * Spinner is an input component to provide a numerical input via increment and decrement buttons.
 * 
 * @prop {number} cursorOffset Index where the number starts in the input field's string value, i.e. after the
 * {@link SpinnerCfg.prefix}.
 * @prop {JQuery} downButton The DOM element for the button that decrements this spinner's value.
 * @prop {JQuery} input The DOM element for the input with the current value.
 * @prop {number} timer The set-timeout ID for the timer for incrementing or decrementing this spinner when an arrow key
 * is pressed.
 * @prop {JQuery} upButton The DOM element for the button that increments this spinner's value.
 * @prop {number} value The current numerical value of this spinner.
 * 
 * @interface {PrimeFaces.widget.SpinnerCfg} cfg The configuration for the {@link  Spinner| Spinner 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 {string} cfg.decimalPlaces Number of decimal places.
 * @prop {string} cfg.decimalSeparator The character separating the integral and fractional parts of the number.
 * @prop {number} cfg.max Minimum allowed value for this spinner.
 * @prop {number} cfg.maxlength Maximum number of characters that may be entered in this field.
 * @prop {number} cfg.min Minimum allowed value for this spinner.
 * @prop {number} cfg.precision The number of digits to appear after the decimal point. 
 * @prop {string} cfg.prefix Prefix added to the displayed value.
 * @prop {boolean} cfg.required Whether this spinner is a required field.
 * @prop {boolean} cfg.rotate Rotate to the minimum value when maximum value is reached and vice versa.
 * @prop {number} cfg.step Stepping factor for each increment and decrement
 * @prop {string} cfg.suffix Suffix added to the displayed value.
 * @prop {string} cfg.thousandSeparator Character for the integral part of the number that separates each group of three
 * digits.
 */
PrimeFaces.widget.Spinner = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.input = this.jq.children('.ui-spinner-input');
        this.upButton = this.jq.children('a.ui-spinner-up');
        this.downButton = this.jq.children('a.ui-spinner-down');
        this.cfg.step = this.cfg.step || 1;
        if (this.cfg.thousandSeparator == undefined) {
          this.cfg.thousandSeparator = '';
        }
        if (!this.cfg.decimalSeparator) {
          this.cfg.decimalSeparator = '.';
        }
        this.cursorOffset = this.cfg.prefix ? this.cfg.prefix.length: 0;

        var inputValue = this.input.val();

        this.cfg.precision = 0;
        var decPlaces = parseInt(this.cfg.decimalPlaces, 10);
        if(decPlaces > 0) {
            this.cfg.precision = decPlaces;
        }
        else if(!(typeof this.cfg.step === 'number' && this.cfg.step % 1 === 0)) {
            this.cfg.precision = this.cfg.step.toString().split(/[,]|[.]/)[1].length;
        }

        var maxlength = this.input.attr('maxlength');
        if(maxlength) {
            this.cfg.maxlength = parseInt(maxlength);
        }

        this.value = this.parseValue(inputValue);

        this.format();

        this.addARIA();

        if(this.input.prop('disabled')||this.input.prop('readonly')) {
            return;
        }

        this.bindEvents();

        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);

        PrimeFaces.skinInput(this.input);
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.jq.children('.ui-spinner-button')
            .on('mouseover.spinner', function() {
                $(this).addClass('ui-state-hover');
            })
            .on('mouseout.spinner', function() {
                $(this).removeClass('ui-state-hover ui-state-active');

                if($this.timer) {
                    clearInterval($this.timer);
                }
            })
            .on('mouseup.spinner', function() {
                clearInterval($this.timer);
                $(this).removeClass('ui-state-active').addClass('ui-state-hover');
                $this.input.trigger('change');
            })
            .on('mousedown.spinner', function(e) {
                var element = $(this),
                dir = element.hasClass('ui-spinner-up') ? 1 : -1;

                element.removeClass('ui-state-hover').addClass('ui-state-active');

                if($this.input.is(':not(:focus)')) {
                    $this.input.trigger('focus');
                }

                $this.repeat(null, dir);

                //keep focused
                e.preventDefault();
        });

        this.input.on('keydown.spinner', function (e) {
            var keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.UP:
                    $this.spin(1);
                break;

                case keyCode.DOWN:
                    $this.spin(-1);
                break;

                case keyCode.ENTER:
                    $this.updateValue();
                    $this.format();
                break;

                case keyCode.BACKSPACE:
                case keyCode.DELETE:
                case keyCode.LEFT:
                case keyCode.RIGHT:
                    return;
                break;

                default:
                    //do nothing
                break;
            }

            /* Github #1964 do not allow minus */
            var isNegative = event.key === '-';
            if ($this.cfg.min >= 0 && isNegative) {
                e.preventDefault();
                return;
            }

            /* GitHub #5579 do not allow decimal separator for integers */
            var isDecimalSeparator = event.key === $this.cfg.decimalSeparator;
            if (isDecimalSeparator && $this.cfg.precision === 0) {
                e.preventDefault();
                return;
            }

            /* GitHub #5579 prevent non numeric characters and duplicate separators */
            var value = $(this).val();
            var isNumber = isFinite(event.key);
            var isThousandsSeparator = event.key === $this.cfg.thousandSeparator;
            if ((isNegative && value.indexOf('-') != -1) 
                    || (isDecimalSeparator && value.indexOf($this.cfg.decimalSeparator)!= -1)
                    || (isThousandsSeparator && value.indexOf($this.cfg.thousandSeparator)!= -1)) {
                e.preventDefault();
                return;
            } 

            if (!isNumber && !(isNegative || isDecimalSeparator || isThousandsSeparator)) {
                e.preventDefault();
                return;
            }
        })
        .on('keyup.spinner', function (e) {
            $this.updateValue();

            var keyCode = $.ui.keyCode;

            /* Github #2636 */
            var checkForIE = (PrimeFaces.env.isIE(11) || PrimeFaces.env.isLtIE(11)) && (e.which === keyCode.ENTER);

            if(e.which === keyCode.UP||e.which === keyCode.DOWN||checkForIE) {
                $this.input.trigger('change');
                $this.format();
            }
        })
        .on('blur.spinner', function(e) {
            $this.format();
        })
        .on('mousewheel.spinner', function(event, delta) {
            if($this.input.is(':focus')) {
                if(delta > 0)
                    $this.spin(1);
                else
                    $this.spin(-1);
                
                $this.input.trigger('change');

                return false;
            }
        });
    },

    /**
     * Increments or decrements this spinner rapidly, at a rate of one step each few frames. Used when the user keeps
     * pressing the up or down arrow button.
     * @private
     * @param {number} interval Initial delay in milliseconds, applied after the first increment or decrement, before
     * this spinner starts incrementing or decrementing rapidly.
     * @param {-1 | 1} dir `-1` to decrement this spinner, or `+1` to increment this spinner.
     */
    repeat: function(interval, dir) {
        var $this = this,
        i = interval||500;

        clearTimeout(this.timer);
        this.timer = setTimeout(function() {
            $this.repeat(40, dir);
        }, i);

        this.spin(dir);
    },

    /**
     * Increments or decrements this spinner by one {@link SpinnerCfg.step}.
     * @param {-1 | 1} dir `-1` to decrement this spinner, or `+1` to increment this spinner.
     */
    spin: function(dir) {
        var step = this.cfg.step * dir,
        currentValue = this.value ? this.value : 0,
        newValue = this.parseValue(currentValue + step);

        if(this.cfg.maxlength !== undefined && newValue.toString().length > this.cfg.maxlength) {
            newValue = currentValue;
        }

        this.value = newValue;
        this.format();
        this.input.attr('aria-valuenow', this.getValue());
    },

    /**
     * Callback for when the value of the input was changed. Parses the current values and saves it.
     * @private
     */
    updateValue: function() {
        var value = this.input.val();

        if(this.cfg.prefix && value.indexOf(this.cfg.prefix) === 0) {
            value = value.substring(this.cfg.prefix.length, value.length);
        }  else {
            var ix = value.indexOf(this.cfg.suffix);
            if(this.cfg.suffix && ix > -1 && ix === (value.length - this.cfg.suffix.length)) {
                value = value.substring(0, value.length - this.cfg.suffix.length);
            }
        }

        value = value.replace(new RegExp(PrimeFaces.escapeRegExp(this.cfg.thousandSeparator), 'g'), '');
        value = value.replace(new RegExp(PrimeFaces.escapeRegExp(this.cfg.decimalSeparator), 'g'), '\.');
        this.value = this.parseValue(value);
    },

    /**
     * Takes the string representation of a number, parses it and restricts it to the limits imposed by the 
     * {@link SpinnerCfg|configuration of this widget}.
     * @private
     * @param {string} value String to parse as a number.
     * @return {number | null} The parsed value, clamped to the allowed range, or `null` if the value could not be
     * parsed.
     */
    parseValue: function(value) {
        var parsedValue;
        if(this.cfg.precision) {
            parsedValue = parseFloat(value);
        } else {
            parsedValue = parseInt(value);
        }
        if(isNaN(parsedValue)) {
            if(PrimeFaces.trim(value) === '' && this.cfg.min !== undefined && this.cfg.required) {
                parsedValue = this.cfg.min;
            } else {
                parsedValue = null;
            }
        } else {
            var minimum = this.cfg.min;
            var maximum = this.cfg.max;

            if (this.cfg.rotate) {
                if(parsedValue < minimum) {
                    parsedValue = maximum;
                }
                if(parsedValue > maximum) {
                    parsedValue = minimum;
                }
            } else {
                if(parsedValue > maximum) {
                    parsedValue = maximum;
                }
                if(parsedValue < minimum) {
                    parsedValue = minimum;
                }
            }
        }
        return parsedValue;
    },

    /**
     * Takes the current numerical value of this spinner, formats it according to the
     * {@link SpinnerCfg|configuration of this widget}, and writes the result to the input field.
     * @private
     */
    format: function() {
        if(this.value !== null) {
            var value = this.getValue();
            var numAndFract = value.toString().split('.');
            value = numAndFract[0].replace(/(\d)(?=(?:\d{3})+\b)/g, '$1' + this.cfg.thousandSeparator);
            if (numAndFract.length === 2) {
              value += this.cfg.decimalSeparator + numAndFract[1];
            }
            value = this.roundStep(value);
            if(this.cfg.prefix)
                value = this.cfg.prefix + value;

            if(this.cfg.suffix)
                value = value + this.cfg.suffix;

            this.input.val(value);
        }
    },

    /**
     * If roundStep is enabled then round to the nearest step value. 
     * For example if step=5 and value=8 it would be rounded 10.
     * @private
     * @param {number} value The value for this spinner.
     * @return {number} Original value if rounding disabled, else a rounded value.
     */
    roundStep: function(value) {
        if (!this.cfg.round) {
            return value;
        }
        return (Math.ceil(value / this.cfg.step) * this.cfg.step).toFixed(this.cfg.precision);
    },

    /**
     * Adds the required ARIA attributes to the elements of this spinner.
     * @private
     */
    addARIA: function() {
        this.input.attr('role', 'spinbutton');
        this.input.attr('aria-multiline', false);
        this.input.attr('aria-valuenow', this.getValue());

        if(this.cfg.min !== undefined)
            this.input.attr('aria-valuemin', this.cfg.min);

        if(this.cfg.max !== undefined)
            this.input.attr('aria-valuemax', this.cfg.max);

        if(this.input.prop('disabled'))
            this.input.attr('aria-disabled', true);

        if(this.input.prop('readonly'))
            this.input.attr('aria-readonly', true);
    },

    /**
     * Reads and returns the value of this spinner.
     * @return {number} The current numerical value of this spinner.
     */
    getValue: function() {
        if(this.cfg.precision) {
            return parseFloat(this.value).toFixed(this.cfg.precision);
        }
        else {
            return this.value;
        }
    },

    /**
     * Sets the value of this spinner to the given number.
     * @param {number} value The new value for this spinner.
     */
    setValue: function(value) {
        this.value = value;
        this.format();
    }
});

/**
 * __PrimeFaces Splitter Widget__
 *
 * Splitter represents entities using icons, labels and images.
 *
 * @typedef {"horizontal" | "vertical"} PrimeFaces.widget.Splitter.Layout Defines how the panel are split.
 * - `horizontal`: The two panels are split in two horizontally by the splitter.
 * - `vertically`: The two panels are split in two vertically by the splitter.
 * 
 * @typedef {"local" | "session"} PrimeFaces.widget.Splitter.StateStorage Defines where to store the current position of the
 * splitter so that it can be restored later.
 * - `local`: Use the browser's local storage which keeps data between sessions.
 * - `session`: Use the browser's session storage which is cleared when the session ends.
 * 
 * @prop {JQuery} panels DOM elements of the splitter panels in splitter.
 * @prop {JQuery} gutters DOM elements of the gutter elements in splitter.
 * @prop {boolean} horizontal Whether splitter element is horizontal or vertical.
 * @prop {number[]} panelSizes Array of the panels size for save and restore state.
 *
 * @interface {PrimeFaces.widget.SplitterCfg} cfg The configuration for the {@link  Splitter| Splitter 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 {number} cfg.gutterSize Defines Size of the divider in pixels.
 * @prop {PrimeFaces.widget.Splitter.Layout} cfg.layout Defines orientation of the panels, whether the panels are split
 * horizontally or vertically.
 * @prop {string} cfg.stateKey Defines storage identifier of a stateful Splitter.
 * @prop {PrimeFaces.widget.Splitter.StateStorage} cfg.stateStorage Defines where a stateful splitter keeps its state.
 */
PrimeFaces.widget.Splitter = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.panels = this.jq.children('.ui-splitter-panel');
        this.gutters = this.jq.children('.ui-splitter-gutter');
        this.panelSizes = [];
        this.horizontal = this.cfg.layout === 'horizontal';
        
        this.initPanelSize();
        this.bindGutterEvent();
    },
    
    /**
     * Initialize panels size.
     * @private
     */
    initPanelSize: function() {
        var $this = this;
        var initialized = false;

        if (this.isStateful()) {
            initialized = this.restoreState();
        }

        if (!initialized) {
            this.panels.each(function(i, panel) {
                var panelInitialSize = panel.dataset && panel.dataset.size;
                var panelSize = panelInitialSize || (100 / $this.panels.length);
                $this.panelSizes[i] = panelSize;
                panel.style.flexBasis = 'calc(' + panelSize + '% - ' + (($this.panels.length - 1) * $this.cfg.gutterSize) + 'px)';
            });
        }
    },

    /**
     * Bind document events
     * @private
     */
    bindDocumentEvents: function() {
        var $this = this;
        
        $(document).on('mousemove.splitter', function(event) {
                $this.onResize(event);
            })
            .on('mouseup.splitter', function(event) {
                $this.onResizeEnd(event);
                $this.unbindDocumentEvents();
            });
    },
    
    /**
     * Removes document events
     * @private
     */
    unbindDocumentEvents: function() {
        $(document).off('mousemove.splitter mouseup.splitter');
    },
    
    /**
     * Set up event for the gutters.
     * @private
     */
    bindGutterEvent: function() {
        var $this = this;
        
        this.gutters.off('mousedown.splitter touchstart.splitter touchmove.splitter touchend.splitter')
            .on('mousedown.splitter', function(event) {
                $this.onResizeStart(event);
                $this.bindDocumentEvents();
            })
            .on('touchstart.splitter', function(event) {
                $this.onResizeStart(event);
                event.preventDefault();
            })
            .on('touchmove.splitter', function(event) {
                $this.onResize(event);
                event.preventDefault();
            })
            .on('touchend.splitter', function(event) {
                $this.onResizeEnd(event);
                event.preventDefault();
            });
    },
    
    /**
     * The method that is called when the 'resize' event starts.
     * @private
     * @param {JQuery.TriggeredEvent} event Event triggered for the drag.
     */
    onResizeStart: function(event) {
        this.gutterElement = $(event.currentTarget);
        this.size = this.horizontal ? this.jq.width() : this.jq.height();
        this.dragging = true;
        this.startPos = this.horizontal ? event.pageX : event.pageY;
        this.prevPanelElement = this.gutterElement.prev();
        this.nextPanelElement = this.gutterElement.next();
        this.prevPanelSize = 100 * (this.horizontal ? this.prevPanelElement.outerWidth(true) : this.prevPanelElement.outerHeight(true)) / this.size;
        this.nextPanelSize = 100 * (this.horizontal ? this.nextPanelElement.outerWidth(true) : this.nextPanelElement.outerHeight(true)) / this.size;
        this.prevPanelIndex = this.panels.index(this.prevPanelElement);
        this.gutterElement.addClass('ui-splitter-gutter-resizing');
        this.jq.addClass('ui-splitter-resizing');
    },

    /**
     * The method called while the 'resize' event is running.
     * @private
     * @param {JQuery.TriggeredEvent} event Event triggered for the resize.
     */
    onResize: function(event) {
        var newPos;

        if (this.horizontal)
            newPos = (event.pageX * 100 / this.size) - (this.startPos * 100 / this.size);
        else
            newPos = (event.pageY * 100 / this.size) - (this.startPos * 100 / this.size);

        var newPrevPanelSize = this.prevPanelSize + newPos;
        var newNextPanelSize = this.nextPanelSize - newPos;

        if (this.validateResize(newPrevPanelSize, newNextPanelSize)) {
            this.prevPanelElement.css('flexBasis', 'calc(' + newPrevPanelSize + '% - ' + ((this.panels.length - 1) * this.cfg.gutterSize) + 'px)');
            this.nextPanelElement.css('flexBasis', 'calc(' + newNextPanelSize + '% - ' + ((this.panels.length - 1) * this.cfg.gutterSize) + 'px)');
            this.panelSizes[this.prevPanelIndex] = newPrevPanelSize;
            this.panelSizes[this.prevPanelIndex + 1] = newNextPanelSize;
        }
    },

    /**
     * The method that is called when the 'resize' event ends.
     * @private
     * @param {JQuery.TriggeredEvent} event Event triggered for the resize end.
     */
    onResizeEnd: function(event) {
        if (this.isStateful()) {
            this.saveState();
        }

        this.gutterElement.removeClass('ui-splitter-gutter-resizing');
        this.jq.removeClass('ui-splitter-resizing');
        this.clear();
    },
    
    /**
     * Clear all variables
     * @private
     */
    clear: function() {
        this.dragging = false;
        this.size = null;
        this.startPos = null;
        this.prevPanelElement = null;
        this.nextPanelElement = null;
        this.prevPanelSize = null;
        this.nextPanelSize = null;
        this.gutterElement = null;
        this.prevPanelIndex = null;
    },
    
    /**
     * Checks the new values according to the size and minimum size values
     * @private
     * @param {number} newPrevPanelSize The new previous panel size.
     * @param {number} newNextPanelSize The new next panel size.
     * @return {boolean} `true` if resized, `false` if not.
     */
    validateResize: function(newPrevPanelSize, newNextPanelSize) {
        if (this.panels[0].dataset && parseFloat(this.panels[0].dataset.minsize) > newPrevPanelSize) {
            return false;
        }

        if (this.panels[1].dataset && parseFloat(this.panels[1].dataset.minsize) > newNextPanelSize) {
            return false;
        }

        return true;
    },

    /**
     * Whether the splitter keeps its dimensions between different page loads.
     * @return {boolean} Whether the splitter is retaining its state.
     */
    isStateful: function() {
        return this.cfg.stateKey != null;
    },
    
    /**
     * Save current panel sizes to the (local or session) storage.
     * @private
     */
    saveState: function() {
        this.getStorage().setItem(this.cfg.stateKey, JSON.stringify(this.panelSizes));
    },

    /**
     * Restore panel sizes from (local or session) storage.
     * @return {boolean} `true` when the state restore operation was successful, `false` otherwise.
     */
    restoreState: function() {
        var storage = this.getStorage();
        var stateString = storage.getItem(this.cfg.stateKey);
        var $this = this;

        if (stateString) {
            this.panelSizes = JSON.parse(stateString);
            this.panels.each(function(i, panel) {
                panel.style.flexBasis = 'calc(' + $this.panelSizes[i] + '% - ' + (($this.panels.length - 1) * $this.cfg.gutterSize) + 'px)';
            });
            
            return true;
        }
        
        return false;
    },

    /**
     * Returns either the local storage or session storage, depending on the current widget configuration.
     * @return {Storage} The storage to be used.
     */
    getStorage: function() {
        switch(this.cfg.stateStorage) {
            case 'local':
                return window.localStorage;

            case 'session':
                return window.sessionStorage;

            default:
                throw new Error(this.cfg.stateStorage + ' is not a valid value for the state storage, supported values are "local" and "session".');
        }
    }
});

/**
 * __PrimeFaces Spotlight Widget__
 * 
 * Spotlight highlights a certain component on page, drawing the user's attention to it.
 * 
 * @prop {JQuery} target The DOM element for the target component to highlight.
 * 
 * @interface {PrimeFaces.widget.SpotlightCfg} cfg The configuration for the {@link  Spotlight| Spotlight 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.active Whether the spotlight is initially turned on.
 * @prop {boolean} cfg.blockScroll `true` to block scrolling when the spotlight is turned on, or `false` otherwise.
 * @prop {string} cfg.target The search expression for the target component to highlight.
 */
PrimeFaces.widget.Spotlight = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.target = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.target);

        if(!$(document.body).children('.ui-spotlight').length) {
            this.createMasks();
        }

        if(this.cfg.active) {
            this.show();
        }
    },

    /**
     * Creates the mask overlay element for the spotlight effect and adds it to the DOM.
     * @private
     */
    createMasks: function() {
        var documentBody = $(document.body);
        documentBody.append('<div class="ui-widget-overlay ui-spotlight ui-spotlight-top ui-helper-hidden"></div><div class="ui-widget-overlay ui-spotlight ui-spotlight-bottom ui-helper-hidden"></div>' +
                        '<div class="ui-widget-overlay ui-spotlight ui-spotlight-left ui-helper-hidden"></div><div class="ui-widget-overlay ui-spotlight ui-spotlight-right ui-helper-hidden"></div>');
    },

    /**
     * Turns the spotlight on so that a certain part of the page is highlighted.
     */
    show: function() {
        this.calculatePositions();

        this.target.attr({
            'role': 'dialog'
            ,'aria-modal': true
        });
        $(document.body).children('div.ui-spotlight').show();

        this.bindEvents();
    },

    /**
     * Computes and applies the rectangular position of the spotlight.
     * @private
     */
    calculatePositions: function() {
        var doc = $(document),
        documentBody = $(document.body),
        offset = PrimeFaces.utils.calculateRelativeOffset(this.target),
        zindex = PrimeFaces.nextZindex();

        documentBody.children('div.ui-spotlight-top').css({
            'left': '0px',
            'top': '0px',
            'width': documentBody.width() + 'px',
            'height': offset.top + 'px',
            'z-index': zindex
        });

        var bottomTop = offset.top + this.target.outerHeight();
        documentBody.children('div.ui-spotlight-bottom').css({
            'left': '0px',
            'top': bottomTop + 'px',
            'width': documentBody.width() + 'px',
            'height': (doc.height() - bottomTop) + 'px',
            'z-index': zindex
        });

        documentBody.children('div.ui-spotlight-left').css({
            'left': '0px',
            'top': offset.top + 'px',
            'width': offset.left + 'px',
            'height': this.target.outerHeight() + 'px',
            'z-index': zindex
        });

        var rightLeft = offset.left + this.target.outerWidth();
        documentBody.children('div.ui-spotlight-right').css({
            'left': rightLeft + 'px',
            'top': offset.top + 'px',
            'width': (documentBody.width() - rightLeft) + 'px',
            'height': this.target.outerHeight() + 'px',
            'z-index': zindex
        });
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.target.data('zindex',this.target.zIndex()).css('z-index', PrimeFaces.nextZindex());

        if (this.cfg.blockScroll) {
            PrimeFaces.utils.preventScrolling();
        }
        PrimeFaces.utils.preventTabbing(this.id, $this.target.zIndex(), function() {
            return $this.target.find(':tabbable');
        });

        $(window).on('resize.spotlight scroll.spotlight', function() {
            $this.calculatePositions();
        });
    },

    /**
     * Removes the event listeners that were added when the spotlight was turned on.
     * @private
     */
    unbindEvents: function() {
        PrimeFaces.utils.enableTabbing(this.id);
        if (this.cfg.blockScroll) {
            PrimeFaces.utils.enableScrolling();
        }
        $(window).off('resize.spotlight scroll.spotlight');
    },

    /**
     * Turns of the spotlight so that the entire page is visible normally again.
     */
    hide: function() {
        $(document.body).children('.ui-spotlight').hide();
        this.unbindEvents();
        this.target.css('z-index', String(this.target.zIndex()));
        this.target.attr({
            'role': ''
            ,'aria-modal': false
        });
    }

});
/**
 * __PrimeFaces Sticky Widget__
 * 
 * Sticky component positions other components as fixed so that these components stay in window viewport during
 * scrolling.
 * 
 * @interface {PrimeFaces.widget.Sticky.InitialState} InitialState Describes some of the initial geometry of the target
 * component before it was made sticky, see {@link Sticky.initialState}.
 * @prop {number} InitialState.height The initial height of the target element. 
 * @prop {number} InitialState.top The initial position of the top edge of the target element.
 * 
 * @prop {boolean} fixed Whether this sticky is currently fixed to the top of the page.
 * @prop {JQuery} ghost The DOM element for the ghost helper element.
 * @prop {PrimeFaces.widget.Sticky.InitialState} initialState The initial position and height of the target component
 * before it was pinned to the page.
 * @prop {JQuery} target The DOM element for the component to be made sticky.
 * 
 * @interface {PrimeFaces.widget.StickyCfg} cfg The configuration for the {@link  Sticky| Sticky 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 {number} margin Margin to the top of the page during fixed scrolling.
 * @prop {string} target The client ID of the component to be made sticky.
 */
PrimeFaces.widget.Sticky = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.target = $(PrimeFaces.escapeClientId(this.cfg.target));
        this.cfg.margin = this.cfg.margin||0;

        this.initialState = {
            top: this.target.offset().top,
            height: this.target.height()
        };

        this.bindEvents();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.target = $(PrimeFaces.escapeClientId(this.cfg.target));

        if(this.fixed) {
            this.ghost.remove();
            this.fix(true);
        }
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this,
        win = $(window);

        PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_align', function() {
            if(win.scrollTop() > $this.initialState.top - $this.cfg.margin)
                $this.fix();
            else
                $this.restore();
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', null, function() {
            if ($this.fixed) {
                $this.target.width($this.ghost.outerWidth() - ($this.target.outerWidth() - $this.target.width()));
            }
        });
    },

    /**
     * Pins this sticky to the page so that it is always visible.
     * @param {boolean} [force] If `true`, pin the sticky irrespective of whether it is pinned already.
     */
    fix: function(force) {
        if(!this.fixed || force) {
            var win = $(window),
            winScrollTop = win.scrollTop();

            this.target.css({
                'position': 'fixed',
                'top': this.cfg.margin + 'px',
                'z-index': PrimeFaces.nextZindex()
            })
            .addClass('ui-shadow ui-sticky');

            this.ghost = $('<div class="ui-sticky-ghost"></div>').height(this.target.outerHeight()).insertBefore(this.target);
            this.target.width(this.ghost.outerWidth() - (this.target.outerWidth() - this.target.width()));
            this.fixed = true;
            win.scrollTop(winScrollTop);
        }
    },

    /**
     * Unpins this sticky and returns it to its normal position.
     */
    restore: function() {
        if(this.fixed) {
            this.target.css({
                position: 'static',
                top: 'auto',
                width: 'auto'
            })
            .removeClass('ui-shadow ui-sticky');

            this.ghost.remove();
            this.fixed = false;
        }
    }

});

/**
 * __PrimeFaces TabView Widget__
 *
 * TabView is a container component to group content in tabs.
 *
 * @typedef PrimeFaces.widget.TabView.OnTabChangeCallback Client side callback to execute when a tab is clicked. If the
 * callback returns `false`, the tab is not selected. See also {@link TabViewCfg.onTabChange}.
 * @this {PrimeFaces.widget.TabView} PrimeFaces.widget.TabView.OnTabChangeCallback
 * @param {number} PrimeFaces.widget.TabView.OnTabChangeCallback.index 0-based index of the tab that is about to be
 * selected.
 * @return {boolean} PrimeFaces.widget.TabView.OnTabChangeCallback `true` to switch to the tab, `false` to stay at the
 * current tab.
 *
 * @typedef PrimeFaces.widget.TabView.OnTabCloseCallback Client side callback to execute on tab close. When the callback
 * returns `false`, the tab is not closed. See also {@link TabViewCfg.onTabClose}.
 * @this {PrimeFaces.widget.TabView} PrimeFaces.widget.TabView.OnTabCloseCallback
 * @param {number} PrimeFaces.widget.TabView.OnTabCloseCallback.index 0-based index of the tab that is about to be
 * closed.
 * @return {boolean} PrimeFaces.widget.TabView.OnTabCloseCallback `true` to close the tab, `false` to keep the tab
 * open.
 *
 * @typedef PrimeFaces.widget.TabView.OnTabShowCallback Client side callback to execute when a tab is shown. See also
 * {@link TabViewCfg.onTabShow}.
 * @this {PrimeFaces.widget.TabView} PrimeFaces.widget.TabView.OnTabShowCallback
 * @param {number} PrimeFaces.widget.TabView.OnTabShowCallback.index 0-based index of the tab that was
 * shown.
 *
 * @prop {JQuery} firstTab The DOM element for the first tab.
 * @prop {JQuery} focusedTab The DOM element for the tab that is currently focused.
 * @prop {JQuery} headerContainer The DOM element for the container element with the tab header.
 * @prop {JQuery} lastTab The DOM element for the last tab.
 * @prop {JQuery} navscroller The DOM element for the tab navigation bar.
 * @prop {JQuery} navcrollerLeft The DOM element for the button that scrolls the tab navigation bar to the left.
 * @prop {JQuery} navcrollerRight The DOM element for the button that scrolls the tab navigation bar to the right.
 * @prop {JQuery} navContainer The DOM element for the container element with the tab navigation bar.
 * @prop {JQuery} panelContainer The DOM element for the panel with the tab's contents.
 * @prop {JQuery} scrollStateHolder The DOM element for the hidden input field storing the current scroll position.
 * @prop {JQuery} stateHolder The DOM element for the hidden input field storing which is tab is active and visible.
 * @prop {number} tabindex Position of the element in the tabbing order.
 *
 * @interface {PrimeFaces.widget.TabViewCfg} cfg The configuration for the {@link  TabView| TabView 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.DeferredWidgetCfg} cfg
 *
 * @prop {boolean} cfg.cache When tab contents are lazy loaded via AJAX toggle mode, caching only retrieves the tab
 * contents once and subsequent toggles of a cached tab does not communicate with server. If caching is turned off, tab
 * contents are reloaded from server each time tab is clicked.
 * @prop {boolean} cfg.dynamic Enables lazy loading of inactive tabs.
 * @prop {string} cfg.effect Name of the transition effect.
 * @prop {number} cfg.effectDuration Duration of the transition effect.
 * @prop {PrimeFaces.widget.TabView.OnTabChangeCallback} cfg.onTabChange Client side callback to execute when a tab is
 * clicked. If the callback returns `false`, the tab is not selected.
 * @prop {PrimeFaces.widget.TabView.OnTabCloseCallback} cfg.onTabClose Client side callback to execute on tab close.
 * When the callback returns `false`, the tab is not closed.
 * @prop {PrimeFaces.widget.TabView.OnTabShowCallback} cfg.onTabShow Client side callback to execute when a tab is
 * shown.
 * @prop {boolean} cfg.scrollable When enabled, tab headers can be scrolled horizontally instead of wrapping.
 * @prop {number} cfg.selected The currently selected tab.
 * @prop {number} cfg.tabindex Position of the element in the tabbing order.
 * @prop {boolean} cfg.multiViewState Whether to keep TabView state across views.
 */
PrimeFaces.widget.TabView = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.panelContainer = this.jq.children('.ui-tabs-panels');
        this.stateHolder = $(this.jqId + '_activeIndex');
        this.cfg.selected = parseInt(this.stateHolder.val());
        this.focusedTabHeader = null;
        this.tabindex = this.cfg.tabindex||0;

        if(this.cfg.scrollable) {
            this.navscroller = this.jq.children('.ui-tabs-navscroller');
            this.navcrollerLeft = this.navscroller.children('.ui-tabs-navscroller-btn-left');
            this.navcrollerRight = this.navscroller.children('.ui-tabs-navscroller-btn-right');
            this.navContainer = this.navscroller.children('.ui-tabs-nav');
            this.firstTab = this.navContainer.children('li.ui-tabs-header:first-child');
            this.lastTab = this.navContainer.children('li.ui-tabs-header:last-child');
            this.scrollStateHolder = $(this.jqId + '_scrollState');
        }
        else {
            this.navContainer = this.jq.children('.ui-tabs-nav');
        }

        this.headerContainer = this.navContainer.children('li.ui-tabs-header');

        this.bindEvents();

        //Cache initial active tab
        if(this.cfg.dynamic && this.cfg.cache) {
            this.markAsLoaded(this.panelContainer.children().eq(this.cfg.selected));
        }

        this.renderDeferred();
    },

    /**
     * @override
     * @inheritdoc
     */
    renderDeferred: function() {
        if(this.jq.is(':visible')) {
            this._render();
        }
        else if (this.jq.parent()[0]) {
            var container = this.jq.parent()[0].closest('.ui-hidden-container');
            if (container) {
                var $container = $(container);
                if ($container.length) {
                    var $this = this;
                    this.addDeferredRender(this.id, $container, function() {
                        return $this.render();
                    });
                }
            }
        }
    },

    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        if(this.cfg.scrollable) {
            this.initScrolling();

            var $this = this;

            PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', null, function() {
                $this.initScrolling();
            });
        }
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        //Tab header events
        this.headerContainer
                .on('mouseover.tabview', function(e) {
                    var element = $(this);
                    if(!element.hasClass('ui-state-disabled')) {
                        element.addClass('ui-state-hover');
                    }
                })
                .on('mouseout.tabview', function(e) {
                    var element = $(this);
                    if(!element.hasClass('ui-state-disabled')) {
                        element.removeClass('ui-state-hover');
                    }
                })
                .on('click.tabview', function(e) {
                    var element = $(this);

                    if($(e.target).is(':not(.ui-icon-close)')) {
                        var index = $this.headerContainer.index(element);

                        if(!element.hasClass('ui-state-disabled') && index !== $this.cfg.selected) {
                            $this.select(index);
                        }
                    }

                    e.preventDefault();
                });

        //Closable tabs
        this.navContainer.find('li .ui-icon-close')
            .on('click.tabview', function(e) {
                var index = $(this).parent().index();

                if($this.cfg.onTabClose) {
                    var retVal = $this.cfg.onTabClose.call($this, index);

                    if(retVal !== false) {
                        $this.remove(index);
                    }
                }
                else {
                    $this.remove(index);
                }

                e.preventDefault();
            });

        //Scrolling
        if(this.cfg.scrollable) {
            this.navscroller.children('.ui-tabs-navscroller-btn')
                            .on('mouseover.tabview', function() {
                                var el = $(this);
                                if(!el.hasClass('ui-state-disabled'))
                                    $(this).addClass('ui-state-hover');
                            })
                            .on('mouseout.tabview', function() {
                                var el = $(this);
                                if(!el.hasClass('ui-state-disabled'))
                                    $(this).removeClass('ui-state-hover ui-state-active');
                            })
                            .on('mousedown.tabview', function() {
                                var el = $(this);
                                if(!el.hasClass('ui-state-disabled'))
                                    $(this).removeClass('ui-state-hover').addClass('ui-state-active');
                            })
                            .on('mouseup.tabview', function() {
                                var el = $(this);
                                if(!el.hasClass('ui-state-disabled'))
                                    $(this).addClass('ui-state-hover').removeClass('ui-state-active');
                            })
                            .on('focus.tabview', function() {
                                $(this).addClass('ui-state-focus');
                            })
                            .on('blur.tabview', function() {
                                $(this).removeClass('ui-state-focus');
                            });


            this.navcrollerLeft.on('click.tabview', function(e) {
                                $this.scroll(100);
                                e.preventDefault();
                            });

            this.navcrollerRight.on('click.tabview', function(e) {
                                $this.scroll(-100);
                                e.preventDefault();
                            });
        }

        this.bindSwipeEvents();
        this.bindKeyEvents();
    },

    /**
     * Binds swipe events to this tabview.
     * @private
     */
    bindSwipeEvents: function() {
        if (!PrimeFaces.env.isTouchable(this.cfg)) {
            return;
        }
        var $this = this;
        this.jq.swipe({
            swipeLeft:function(event) {
                var activeIndex = $this.getActiveIndex();
                if (activeIndex < $this.getLength() - 1) {
                    $this.select(activeIndex + 1);
                }
            },
            swipeRight: function(event) {
                var activeIndex = $this.getActiveIndex();
                if (activeIndex > 0) {
                    $this.select(activeIndex - 1);
                }
            },
            excludedElements: PrimeFaces.utils.excludedSwipeElements()
        });
    },

   /**
    * Sets up all keyboard related event listeners that are required by this widget.
    * @private
    */
   bindKeyEvents: function() {
        var $this = this,
            tabs = this.headerContainer;

        /* For Screen Reader and Keyboard accessibility */
        tabs.attr('tabindex', this.tabindex);

        tabs.on('focus.tabview', function(e) {
            var focusedTab = $(this);

            if(!focusedTab.hasClass('ui-state-disabled')) {
                focusedTab.addClass('ui-tabs-outline');

                if($this.cfg.scrollable) {
                    if(focusedTab.position().left + focusedTab.width() > $this.navcrollerRight.position().left) {
                        $this.navcrollerRight.trigger('click.tabview');
                    }
                    else if(focusedTab.position().left < $this.navcrollerLeft.position().left) {
                        $this.navcrollerLeft.trigger('click.tabview');
                    }
                }
            }
        })
        .on('blur.tabview', function(){
            $(this).removeClass('ui-tabs-outline');
        })
        .on('keydown.tabview', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which,
            element = $(this);

            if((key === keyCode.SPACE || key === keyCode.ENTER) && !element.hasClass('ui-state-disabled')) {
                $this.select(element.index());
                e.preventDefault();
            }
        });

        //Scrolling
        if(this.cfg.scrollable) {
            this.navcrollerLeft.on('keydown.tabview', function(e) {
                var keyCode = $.ui.keyCode,
                key = e.which;

                if(key === keyCode.SPACE || key === keyCode.ENTER) {
                    $this.scroll(100);
                    e.preventDefault();
                }
            });

            this.navcrollerRight.on('keydown.tabview', function(e) {
                var keyCode = $.ui.keyCode,
                key = e.which;

                if(key === keyCode.SPACE || key === keyCode.ENTER) {
                    $this.scroll(-100);
                    e.preventDefault();
                }
            });
        }
    },

    /**
     * Sets up the classes and attributes required for scrolling the tab navigation bar.
     * @private
     */
    initScrolling: function() {
        if(this.headerContainer.length) {
            var overflown = ((this.lastTab.position().left + this.lastTab.width()) - this.firstTab.position().left) > this.navscroller.innerWidth();
            if (overflown) {
                this.navscroller.removeClass('ui-tabs-navscroller-btn-hidden');
                this.navcrollerLeft.attr('tabindex', this.tabindex);
                this.navcrollerRight.attr('tabindex', this.tabindex);
                this.restoreScrollState();
            }
            else {
                this.navscroller.addClass('ui-tabs-navscroller-btn-hidden');
                this.navcrollerLeft.attr('tabindex', this.tabindex);
                this.navcrollerRight.attr('tabindex', this.tabindex);
            }
        }
    },

    /**
     * Scrolls the tab navigation bar by the given amount.
     * @param {number} step Amount to scroll the navigation bar, positive to scroll to the right, negative to scroll to
     * the left.
     */
    scroll: function(step) {
        if(this.navContainer.is(':animated')) {
            return;
        }

        var oldMarginLeft = parseInt(this.navContainer.css('margin-left')),
        newMarginLeft = oldMarginLeft + step,
        viewportWidth = this.navscroller.innerWidth(),
        $this = this;

        if(step < 0) {
            var lastTabBoundry = this.lastTab.position().left + parseInt(this.lastTab.innerWidth());

            if(lastTabBoundry > viewportWidth)
                this.navContainer.animate({'margin-left': newMarginLeft + 'px'}, 'fast', 'easeInOutCirc', function() {
                    $this.saveScrollState(newMarginLeft);

                    if((lastTabBoundry + step) < viewportWidth)
                        $this.disableScrollerButton($this.navcrollerRight);
                    if($this.navcrollerLeft.hasClass('ui-state-disabled'))
                        $this.enableScrollerButton($this.navcrollerLeft);
                });
        }
        else {
            if(newMarginLeft <= 0) {
                this.navContainer.animate({'margin-left': newMarginLeft + 'px'}, 'fast', 'easeInOutCirc', function() {
                    $this.saveScrollState(newMarginLeft);

                    if(newMarginLeft === 0)
                        $this.disableScrollerButton($this.navcrollerLeft);
                    if($this.navcrollerRight.hasClass('ui-state-disabled'))
                        $this.enableScrollerButton($this.navcrollerRight);
                });
            }
        }
    },

    /**
     * Disables the buttons for scrolling the contents of the navigation bar.
     * @param {JQuery} btn The scroll button to enable.
     */
    disableScrollerButton: function(btn) {
        btn.addClass('ui-state-disabled').removeClass('ui-state-hover ui-state-active ui-state-focus').attr('tabindex', -1);
    },

    /**
     * Enables the buttons for scrolling the contents of the navigation bar.
     * @param {JQuery} btn The scroll button to enable.
     */
    enableScrollerButton: function(btn) {
        btn.removeClass('ui-state-disabled').attr('tabindex', this.tabindex);
    },

    /**
     * Stores the current scroll position in a hidden input field, called before an AJAX request.
     * @private
     * @param {number} value The scroll position to be saved.
     */
    saveScrollState: function(value) {
        this.scrollStateHolder.val(value);
    },

    /**
     * Restores the current scroll position in a hidden input field, called after an AJAX request.
     * @private
     */
    restoreScrollState: function() {
        var value = parseInt(this.scrollStateHolder.val());
        if(value === 0) {
            this.disableScrollerButton(this.navcrollerLeft);
        }

        this.navContainer.css('margin-left', this.scrollStateHolder.val() + 'px');
    },

    /**
     * Selects the given tab, if it is not selected already.
     * @param {number} index 0-based index of the tab to select.
     * @param {boolean} [silent] Controls whether events are triggered.
     * @return {boolean} Whether the given tab is now selected.
     */
    select: function(index, silent) {
        //Call user onTabChange callback
        if(this.cfg.onTabChange && !silent) {
            var result = this.cfg.onTabChange.call(this, index);
            if(result === false)
                return false;
        }

        var newPanel = this.panelContainer.children().eq(index),
        shouldLoad = this.cfg.dynamic && !this.isLoaded(newPanel);

        //update state
        this.stateHolder.val(newPanel.data('index'));
        this.cfg.selected = index;

        if(shouldLoad) {
            this.loadDynamicTab(newPanel);
        }
        else {
            this.show(newPanel);

            if (!silent) {
                if (this.hasBehavior('tabChange')) {
                    this.fireTabChangeEvent(newPanel);
                }
                else if (this.cfg.multiViewState) {
                    var options = {
                        source: this.id,
                        partialSubmit: true,
                        partialSubmitFilter: this.id + '_activeIndex',
                        process: this.id,
                        ignoreAutoUpdate: true,
                        params: [
                            {name: this.id + '_activeIndex', value: this.getActiveIndex()}
                        ]
                    };

                    PrimeFaces.ajax.Request.handle(options);
                }
            }
        }

        return true;
    },

    /**
     * After a tab was loaded from the server, prepares the given tab and shows it.
     * @private
     * @param {JQuery} newPanel New tab to be shown.
     */
    show: function(newPanel) {
        var headers = this.headerContainer,
        oldHeader = headers.filter('.ui-state-active'),
        oldActions = oldHeader.next('.ui-tabs-actions'),
        newHeader = headers.eq(newPanel.index()),
        newActions = newHeader.next('.ui-tabs-actions'),
        oldPanel = this.panelContainer.children('.ui-tabs-panel:visible'),
        $this = this;

        //aria
        oldPanel.attr('aria-hidden', true);
        oldPanel.addClass('ui-helper-hidden');
        oldHeader.attr('aria-expanded', false);
        oldHeader.attr('aria-selected', false);
        if(oldActions.length != 0) {
            oldActions.attr('aria-hidden', true);
        }

        newPanel.attr('aria-hidden', false);
        newPanel.removeClass('ui-helper-hidden');
        newHeader.attr('aria-expanded', true);
        newHeader.attr('aria-selected', true);
        if(newActions.length != 0) {
            newActions.attr('aria-hidden', false);
        }

        if(this.cfg.effect) {
            oldPanel.hide(this.cfg.effect, null, this.cfg.effectDuration, function() {
                oldHeader.removeClass('ui-tabs-selected ui-state-active');
                if(oldActions.length != 0) {
                    oldActions.hide($this.cfg.effect, null, $this.cfg.effectDuration);
                }

                newHeader.addClass('ui-tabs-selected ui-state-active');
                newPanel.show($this.cfg.effect, null, $this.cfg.effectDuration, function() {
                    $this.postTabShow(newPanel);
                });
                if(newActions.length != 0) {
                    newActions.show($this.cfg.effect, null, $this.cfg.effectDuration);
                }
            });
        }
        else {
            oldHeader.removeClass('ui-tabs-selected ui-state-active');
            oldPanel.hide();
            if(oldActions.length != 0) {
                oldActions.hide();
            }

            newHeader.addClass('ui-tabs-selected ui-state-active');
            newPanel.show();
            if(newActions.length != 0) {
                newActions.show();
            }

            this.postTabShow(newPanel);
        }
    },

    /**
     * Dynamically loads contents of a tab from the server via AJAX.
     * @private
     * @param {JQuery} newPanel The tab whose content needs to be loaded.
     */
    loadDynamicTab: function(newPanel) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true},
                {name: this.id + '_newTab', value: newPanel.attr('id')},
                {name: this.id + '_tabindex', value: newPanel.data('index')}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            // hide first
                            // otherwise it will already be displayed after replacing the content with .html()
                            if($this.cfg.effect) {
                                newPanel.hide();
                            }
                            newPanel.html(content);

                            if($this.cfg.cache) {
                                $this.markAsLoaded(newPanel);
                            }
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.show(newPanel);
            }
        };

        if(this.hasBehavior('tabChange')) {
            this.callBehavior('tabChange', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Closes the tab at the given index.
     * @param {number} index 0-based index of the tab to close.
     */
    remove: function(index) {
        // remove old header and content
        var header = this.headerContainer.eq(index),
        panel = this.panelContainer.children().eq(index);

        header.remove();
        panel.remove();

        // refresh "chached" selectors
        this.headerContainer = this.navContainer.children('li.ui-tabs-header');
        this.panelContainer = this.jq.children('.ui-tabs-panels');

        // select next tab
        var length = this.getLength();
        if(length > 0) {
            if(index < this.cfg.selected) {
                this.cfg.selected--;
            }
            else if(index === this.cfg.selected) {
                var newIndex = (this.cfg.selected === (length)) ? (this.cfg.selected - 1): this.cfg.selected,
                newPanelHeader = this.headerContainer.eq(newIndex);

                if(newPanelHeader.hasClass('ui-state-disabled')) {
                    var newHeader = this.headerContainer.filter(':not(.ui-state-disabled):first');
                    if(newHeader.length) {
                        this.select(newHeader.index(), true);
                    }
                }
                else {
                    this.select(newIndex, true);
                }
            }
        }
        else {
            this.cfg.selected = -1;
        }

        this.fireTabCloseEvent(panel.attr('id'), index);
    },

    /**
     * Fins the number of tabs of this tab view.
     * @return {number} The number of tabs.
     */
    getLength: function() {
        return this.navContainer.children().length;
    },

    /**
     * Finds and returns the tab that is currently selected.
     * @return {number} The 0-based index of the currently selected tab.
     */
    getActiveIndex: function() {
        return this.cfg.selected;
    },

    /**
     * Calls the appropriate behaviors when a different tab was selected.
     * @private
     * @param {JQuery} panel The tab that was selected.
     */
    fireTabChangeEvent: function(panel) {
        var ext = {
            params: [
                {name: this.id + '_newTab', value: panel.attr('id')},
                {name: this.id + '_tabindex', value: panel.data('index')}
            ]
        };

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

    /**
     * Calls the appropriate behaviors when a tab was closed.
     * @private
     * @param {string} id Client ID of the tab that was closed.
     * @param {number} index 0-based index of the tab that was closed.
     */
    fireTabCloseEvent: function(id, index) {
        if(this.hasBehavior('tabClose')) {
            var ext = {
                params: [
                    {name: this.id + '_closeTab', value: id},
                    {name: this.id + '_tabindex', value: index}
                ]
            };

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

    /**
     * Marks the content of the given tab as loaded.
     * @private
     * @param {JQuery} panel A panel with content that was loaded.
     */
    markAsLoaded: function(panel) {
        panel.data('loaded', true);
    },

    /**
     * If the content of the tab is loaded dynamically via AJAX, checks if the content was loaded already.
     * @private
     * @param {JQuery} panel A panel to check.
     * @return {boolean} Whether the content of the given panel was loaded from the server.
     */
    isLoaded: function(panel) {
        return panel.data('loaded') === true;
    },

    /**
     * Disables the tab at the given index. Disabled tabs may not be selected.
     * @param {number} index 0-based index of the tab to disable.
     */
    disable: function(index) {
        this.headerContainer.eq(index).addClass('ui-state-disabled');
    },

    /**
     * Enables the tab at the given index. Enabled tabs may be selected.
     * @param {number} index 0-based index of the tab to enable.
     */
    enable: function(index) {
        this.headerContainer.eq(index).removeClass('ui-state-disabled');
    },

    /**
     * Callback that is invoked after a tab was shown.
     * @private
     * @param {JQuery} newPanel The panel with the content of the tab.
     */
    postTabShow: function(newPanel) {
        //execute user defined callback
        if(this.cfg.onTabShow) {
            this.cfg.onTabShow.call(this, newPanel.index());
        }

        PrimeFaces.invokeDeferredRenders(this.id);
    }

});

/**
 * __PrimeFaces TagCloud Widget__
 * 
 * TagCloud displays a collection of tag with different strengths.
 * 
 * @interface {PrimeFaces.widget.TagCloudCfg} cfg The configuration for the {@link  TagCloud| TagCloud 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
 */
PrimeFaces.widget.TagCloud = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        var _self = this;

        this.jq.find('a').on("mouseover", function() {
            $(this).addClass('ui-state-hover');
        })
        .on("mouseout", function() {
            $(this).removeClass('ui-state-hover');
        })
        .on("click", function(e) {
            var link = $(this);

            if(link.attr('href') === '#') {
                _self.fireSelectEvent(link);
                e.preventDefault();
            }
        });
    },

    /**
     * Callback for when a tag was clicked. Invokes the appropriate behavior.
     * @private
     * @param {JQuery} link The link element that was clicked. 
     */
    fireSelectEvent: function(link) {
        if(this.hasBehavior('select')) {
            var ext = {
                params: [
                    {name: this.id + '_itemIndex', value: link.parent().index()}
                ]
            };

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

});
/**
 * __PrimeFaces Tooltip Widget__
 * 
 * Tooltip goes beyond the legacy HTML title attribute by providing custom effects, events, HTML content and advance
 * theme support.
 * 
 * @typedef {"right" | "left" | "top" | "bottom"} PrimeFaces.widget.Tooltip.TooltipPosition Position of the tooltip,
 * relative to the target component.
 * 
 * @typedef PrimeFaces.widget.Tooltip.BeforeShowCallback Client side callback to execute before tooltip is  shown.
 * Returning false will prevent display. See also {@link TooltipCfg.beforeShow}.
 * @this {PrimeFaces.widget.Tooltip} PrimeFaces.widget.Tooltip.BeforeShowCallback 
 * @return {boolean} PrimeFaces.widget.Tooltip.BeforeShowCallback `true` to show the tooltip, or `false` to prevent it
 * from being shown.
 * 
 * @typedef PrimeFaces.widget.Tooltip.OnHideCallback Client side callback to execute after tooltip is shown. See also
 * {@link TooltipCfg.onHide}.
 * @this {PrimeFaces.widget.Tooltip} PrimeFaces.widget.Tooltip.OnHideCallback 
 * 
 * @typedef PrimeFaces.widget.Tooltip.OnShowCallback Client side callback to execute after tooltip is shown. See also
 * {@link TooltipCfg.onShow}.
 * @this {PrimeFaces.widget.Tooltip} PrimeFaces.widget.Tooltip.OnShowCallback 
 * 
 * @prop {string} globalTitle The text that is shown as the global title.
 * @prop {JQuery.TriggeredEvent} mouseEvent The mouse event that occurred for this tooltip.
 * @prop {JQuery} target The DOM element for the target component.
 * @prop {number} timeout The set-timeout timer ID of the time for the tooltip delay.
 * 
 * @interface {PrimeFaces.widget.TooltipCfg} cfg The configuration for the {@link  Tooltip| Tooltip 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 {PrimeFaces.widget.Tooltip.BeforeShowCallback} cfg.beforeShow Client side callback to execute before tooltip is
 * shown. Returning false will prevent display.
 * @prop {string} cfg.delegate Search expression for overriding the {@link target}.
 * @prop {boolean} cfg.escape Defines whether HTML would be escaped or not.
 * @prop {string} cfg.globalSelector A jQuery selector for global tooltip, defaults to `a,:input,:button`.
 * @prop {number} cfg.hideDelay Delay time to hide tooltip in milliseconds.
 * @prop {string} cfg.hideEffect Effect to be used for hiding.
 * @prop {number} cfg.hideEffectDuration Delay time to hide tooltip in milliseconds.
 * @prop {string} cfg.hideEvent Event hiding the tooltip.
 * @prop {PrimeFaces.widget.Tooltip.OnHideCallback} cfg.onHide Client side callback to execute after tooltip is shown.
 * @prop {PrimeFaces.widget.Tooltip.OnShowCallback} cfg.onShow Client side callback to execute after tooltip is shown.
 * @prop {PrimeFaces.widget.Tooltip.TooltipPosition} cfg.position Position of the tooltip.
 * @prop {number} cfg.showDelay Delay time to show tooltip in milliseconds.
 * @prop {string} cfg.showEffect Effect to be used for displaying.
 * @prop {string} cfg.showEvent Event displaying the tooltip. 
 * @prop {string} cfg.styleClass Style class of the tooltip.
 * @prop {string} cfg.myPos Position of tooltip with respect to target. If set overrides the 'position' attribute.
 * @prop {string} cfg.atPos Position of tooltip with respect to target. If set overrides the 'position' attribute.
 * @prop {string} cfg.target Search expression for the component to which the tooltip is attached.
 * @prop {boolean} cfg.trackMouse Whether the tooltip position should follow the mouse or pointer.
 */
PrimeFaces.widget.Tooltip = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.showEvent = this.cfg.showEvent ? this.cfg.showEvent + '.tooltip' : 'mouseover.tooltip';
        this.cfg.hideEvent = this.cfg.hideEvent ? this.cfg.hideEvent + '.tooltip' : 'mouseout.tooltip';
        this.cfg.showEffect = this.cfg.showEffect ? this.cfg.showEffect : 'fade';
        this.cfg.hideEffect = this.cfg.hideEffect ? this.cfg.hideEffect : 'fade';
        this.cfg.showDelay = this.cfg.showDelay||150;
        this.cfg.hideDelay = this.cfg.hideDelay||0;
        this.cfg.hideEffectDuration = this.cfg.target ? 250 : 1;
        this.cfg.position = this.cfg.position||'right';
        this.cfg.escape = (this.cfg.escape === undefined) ? true : this.cfg.escape;

        if(this.cfg.target)
            this.bindTarget();
        else
            this.bindGlobal();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        if(cfg.target) {
            var targetTooltip = $(document.body).children(PrimeFaces.escapeClientId(cfg.id));
            if(targetTooltip.length)
                targetTooltip.remove();
        }
        else {
            $(document.body).children('.ui-tooltip-global').remove();
        }

        this._super(cfg);
    },

    /**
     * Sets up all global event listeners that are required for the tooltip.
     * @private
     */
    bindGlobal: function() {
        this.jq = $('<div class="ui-tooltip ui-tooltip-global ui-widget ui-tooltip-' + this.cfg.position + '" role="tooltip"></div>')
            .appendTo('body');
        this.jq.append('<div class="ui-tooltip-arrow"></div><div class="ui-tooltip-text ui-shadow ui-corner-all"></div>');

        this.jq.addClass(this.cfg.styleClass);
        
        this.cfg.globalSelector = this.cfg.globalSelector||'a,:input,:button';
        var $this = this;

        $(document).off(this.cfg.showEvent + ' ' + this.cfg.hideEvent, this.cfg.globalSelector)
                    .on(this.cfg.showEvent, this.cfg.globalSelector, function(e) {
                        var element = $(this);
                        if(element.prop('disabled')) {
                            return;
                        }

                        if($this.cfg.trackMouse) {
                            $this.mouseEvent = e;
                        }

                        var title = element.attr('title');
                        if(title) {
                            element.data('tooltip', title).removeAttr('title');
                        }

                        var arrow = $this.jq.children('.ui-tooltip-arrow');

                        if(element.hasClass('ui-state-error')) {
                            $this.jq.children('.ui-tooltip-text').addClass('ui-state-error');
                            arrow.addClass('ui-state-error');
                        }
                        else {
                            arrow.removeClass('ui-state-error');
                        }

                        var text = element.data('tooltip');
                        if(text) {
                            if($this.cfg.escape)
                                $this.jq.children('.ui-tooltip-text').text(text);
                            else
                                $this.jq.children('.ui-tooltip-text').html(text);

                            $this.globalTitle = text;
                            $this.target = element;
                            $this.show();
                        }
                    })
                    .on(this.cfg.hideEvent + '.tooltip', this.cfg.globalSelector, function() {
                        if($this.globalTitle) {
                            $this.hide();
                            $this.globalTitle = null;
                            $this.target = null;
                            $this.jq.children('.ui-tooltip-text').removeClass('ui-state-error');
                        }
                    });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.tooltip' + '_align', $this.jq, function() {
            $this.align();
        });

    },

    /**
     * Sets up all event listeners on the target component that are required for the tooltip.
     * @private
     */
    bindTarget: function() {
        this.id = this.cfg.id;
        this.jqId = PrimeFaces.escapeClientId(this.id);
        this.jq = $(this.jqId);
        this.target = PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.target);

        var describedBy = this.target.attr("aria-describedby");
        if (!describedBy || 0 === describedBy.length) {
            describedBy = this.id;
        } else {
            describedBy += " " + this.id;
        }
        this.target.attr("aria-describedby", describedBy);

        var $this = this;
        if(this.cfg.delegate) {
            var targetSelector = "*[id='" + this.target.attr('id') + "']";

            $(document).off(this.cfg.showEvent + ' ' + this.cfg.hideEvent, targetSelector)
                        .on(this.cfg.showEvent, targetSelector, function(e) {
                            if($this.cfg.trackMouse) {
                                $this.mouseEvent = e;
                            }

                            if(PrimeFaces.trim($this.jq.children('.ui-tooltip-text').html()) !== '') {
                                $this.show();
                            }
                        })
                        .on(this.cfg.hideEvent + '.tooltip', function() {
                            $this.hide();
                        });
        }
        else {
            this.target.off(this.cfg.showEvent + ' ' + this.cfg.hideEvent)
                        .on(this.cfg.showEvent, function(e) {
                            if($this.cfg.trackMouse) {
                                $this.mouseEvent = e;
                            }

                            if(PrimeFaces.trim($this.jq.children('.ui-tooltip-text').html()) !== '') {
                                $this.show();
                            }
                        })
                        .on(this.cfg.hideEvent + '.tooltip', function() {
                            $this.hide();
                        });
        }

        this.jq.appendTo(document.body);

        if(PrimeFaces.trim(this.jq.children('.ui-tooltip-text').html()) === '') {
            var text = this.target.attr('title');
            if(this.cfg.escape)
                this.jq.children('.ui-tooltip-text').text(text);
            else
                this.jq.children('.ui-tooltip-text').html(text);
        }

        this.target.removeAttr('title');


        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.jq, function() {
            $this.align();
        });
    },

    /**
     * Aligns the position of this tooltip via the given options.
     * @private
     * @param {PrimeFaces.widget.Tooltip.TooltipPosition} position Position where the tooltip should be shown.
     * @param {Record<string, string>} feedback Feedback about the position and dimensions of both elements, as well as
     * calculations to their relative position.
     */
    alignUsing: function(position,feedback) {
        this.jq.removeClass('ui-tooltip-left ui-tooltip-right ui-tooltip-top ui-tooltip-bottom');
        switch (this.cfg.position) {
        case "right":
        case "left":
            this.jq.addClass('ui-tooltip-'+
                    (feedback['horizontal']=='left'?'right':'left'));
            break;
        case "top":
        case "bottom":
            this.jq.addClass('ui-tooltip-'+
                    (feedback['vertical']=='top'?'bottom':'top'));
            break;
        }
        this.jq.css({
            left: position['left'] + 'px',
            top: position['top'] + 'px'
        });
    },

    /**
     * Aligns the position of this tooltip so that it is shown next to the target component.
     */
    align: function() {
        var $this = this;
         this.jq.css({
            left:'',
            top:'',
            'z-index': PrimeFaces.nextZindex()
        });

        if(this.cfg.trackMouse && this.mouseEvent) {
            this.jq.position({
                my: 'left+3 top',
                of: this.mouseEvent,
                collision: 'flipfit',
                using: function(p,f) {
                    $this.alignUsing.call($this,p,f);
                }
            });

            this.mouseEvent = null;
        }
        else {
            var _my = this.cfg.myPos, 
            _at = this.cfg.atPos;

            if (!_my || !_at) {
                switch(this.cfg.position) {
                case 'right':
                    _my = 'left center';
                    _at = 'right center';
                break;

                case 'left':
                    _my = 'right center';
                    _at = 'left center';
                break;

                case 'top':
                    _my = 'center bottom';
                    _at = 'center top';
                break;

                case 'bottom':
                    _my = 'center top';
                    _at = 'center bottom';
                break;
                }
            }

            this.jq.position({
                my: _my,
                at: _at,
                of: this.getTarget(),
                collision: 'flipfit',
                using: function(p,f) {
                    $this.alignUsing.call($this,p,f);
                }
            });
        }
    },

    /**
     * Brings up this tooltip and displays it next to the target component.
     */
    show: function() {
        if(this.getTarget()) {
            var $this = this;
            this.clearTimeout();

            this.timeout = setTimeout(function() {
                $this._show();
            }, this.cfg.showDelay);
        }
    },

    /**
     * Callback for when the tooltip is brought up, also invokes the appropriate behaviors.
     * @private
     */
    _show: function() {
        var $this = this;

        if(this.cfg.beforeShow) {
            var retVal = this.cfg.beforeShow.call(this);
            if(retVal === false) {
                return;
            }
        }

        this.jq.css({'display':'block', 'opacity':'0', 'pointer-events': 'none'});
    
        this.align();

        this.jq.css({'display':'none', 'opacity':'', 'pointer-events': ''});
        
        if(this.cfg.trackMouse) {
            this.followMouse();
        }
        this.jq.show(this.cfg.showEffect, {}, 250, function() {
            if($this.cfg.onShow) {
                $this.cfg.onShow.call();
            }
        });
    },

    /**
     * Hides this tooltip so that it is not shown any longer.
     */
    hide: function() {
        var $this = this;
        this.clearTimeout();

        if(this.cfg.hideDelay) {
            this.timeout = setTimeout(function() {
                $this._hide();
            }, this.cfg.hideDelay);
        }
        else {
            this._hide();
        }
    },

    /**
     * Callback for when the tooltip is hidden, also invokes the appropriate behaviors.
     * @private
     */
    _hide: function() {
        var $this = this;

        if(this.isVisible()) {
            this.jq.hide(this.cfg.hideEffect, {}, this.cfg.hideEffectDuration, function() {
                $(this).css('z-index', '');
                if($this.cfg.trackMouse) {
                    $this.unfollowMouse();
                }

                if($this.cfg.onHide) {
                    $this.cfg.onHide.call();
                }
            });
        }
    },

    /**
     * Clears the current set-timeout timer, if any.
     * @private
     */
    clearTimeout: function() {
        if(this.timeout) {
            clearTimeout(this.timeout);
        }
    },

    /**
     * Adds the event listener for moving the tooltip to the current position of the mouse. Used when the tooltip is
     * brought up.
     * @private
     */
    followMouse: function() {
        var $this = this;

        this.getTarget().on('mousemove.tooltip-track', function(e) {
            $this.jq.position({
                my: 'left+3 top',
                of: e,
                collision: 'flipfit'
            });
        });
    },

    /**
     * Removes the event listener for moving the tooltip to the current position of the mouse. Used when the tooltip
     * is hidden.
     * @private
     */
    unfollowMouse: function() {
        var target = this.getTarget();
        if(target) {
            target.off('mousemove.tooltip-track');
        }
    },

    /**
     * Checks whether this tooltip is visible.
     * @return {boolean} Whether this tooltip is currently shown.
     */
    isVisible: function() {
        return this.jq.is(':visible');
    },

    /**
     * Finds the component for which this tooltip is shown.
     * @private
     * @return {JQuery} The target component for this tooltip.
     */
    getTarget: function() {
        if(this.cfg.delegate)
            return PrimeFaces.expressions.SearchExpressionFacade.resolveComponentsAsSelector(this.cfg.target);
        else
            return this.target;
    }

});

/**
 * __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<PrimeFaces.widget.BaseTree>}
 * 
 * @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 {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 {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {boolean} cfg.highlight `true` if selected nodes are highlighted, or `false` otherwise.
 * @prop {Record<string, PrimeFaces.widget.BaseTree.NodeIconSet>} 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<TCfg>} 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(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('span.ui-treenode-content'));
                                }

                                if(this.cfg.droppable) {
                                    this.makeDropPoints(nodeChildrenContainer.find('li.ui-tree-droppoint'));
                                    this.makeDropNodes(nodeChildrenContainer.find('span.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 > .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";
    }

});

/**
 * __PrimeFaces Vertical Tree Widget__
 * 
 * Tree is used for displaying hierarchical data and creating a site navigation. This implements a vertical tree.
 * 
 * @typedef {"none" | "sibling"} PrimeFaces.widget.VerticalTree.DropRestrictMode Defines parent-child restrictions when
 * a node is dropped.
 * 
 * @interface {PrimeFaces.widget.VerticalTree.DroppedNodeParams} DroppedNodeParams
 * @prop {JQueryUI.DroppableOptions} DroppedNodeParams.ui Details about the drop event.
 * @prop {PrimeFaces.widget.VerticalTree} DroppedNodeParams.dragSource Tree widget of the dragged node.
 * @prop {JQuery} DroppedNodeParams.dragNode The node that was dragged.
 * @prop {JQuery} DroppedNodeParams.targetDragNode The node that was the target of the drag.
 * @prop {JQuery} DroppedNodeParams.dropPoint The drop point where the node was dropped.
 * @prop {JQuery} DroppedNodeParams.dropNode The node on which the dragged node was dropped.
 * @prop {boolean} DroppedNodeParams.transfer Whether a transfer should occur, i.e. whether the node was not dropped on
 * itself.
 * 
 * @prop {JQuery} container The DOM element for the tree container.
 * @prop {PrimeFaces.widget.VerticalTree.DroppedNodeParams[]} droppedNodeParams List of parameter describing the drag &
 * drop operations.
 * @prop {JQuery} filterInput The DOM element for the filter input field that lets the user search the tree.
 * @prop {number} filterTimeout The set-timeout timer ID of the timer for the filter delay.
 * @prop {string[]} invalidSourceKeys A list of row keys for rows that are not valid drag sources.
 * @prop {number} scrollInterval The set-interval time ID of the timer for scrolling. 
 * @prop {JQuery} scrollStateHolder Form element that holds the current scroll state.
 * @prop {boolean} shiftKey For drag&drop, whether the shift is pressed.
 * 
 * @interface {PrimeFaces.widget.VerticalTreeCfg} cfg The configuration for the
 * {@link  VerticalTree| VerticalTree 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.BaseTreeCfg} cfg
 * 
 * @prop {string} cfg.collapsedIcon Named of the icon for collapsed nodes.
 * @prop {boolean} cfg.controlled Whether drag & drop operations of this tree table are controlled.
 * @prop {PrimeFaces.widget.VerticalTree.DropRestrictMode} dropRestrict Defines parent-child restrictions when a node is
 * dropped.
 * @prop {boolean} cfg.rtl `true` if text direction is right-to-left, or `false` otherwise.
 */
PrimeFaces.widget.VerticalTree = PrimeFaces.widget.BaseTree.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.container = this.jq.children('.ui-tree-container');
        this.cfg.rtl = this.jq.hasClass('ui-tree-rtl');
        this.cfg.collapsedIcon = this.cfg.rtl ? 'ui-icon-triangle-1-w' : 'ui-icon-triangle-1-e';
        this.scrollStateHolder = $(this.jqId + '_scrollState');

        if(!this.cfg.disabled) {
            if(this.cfg.draggable) {
                this.initDraggable();
            }

            if(this.cfg.droppable) {
                this.initDroppable();
            }
        }

        this.restoreScrollState();
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindEvents: function() {
        var $this = this,
        togglerSelector = '.ui-tree-toggler',
        nodeLabelSelector = '.ui-tree-selectable .ui-treenode-label',
        nodeContentSelector = '.ui-treenode-content';

        this.jq.off('click.tree-toggle', togglerSelector)
                    .on('click.tree-toggle', togglerSelector, null, function(e) {
                        var toggleIcon = $(this),
                        node = toggleIcon.closest('li');

                        if(toggleIcon.hasClass($this.cfg.collapsedIcon))
                            $this.expandNode(node);
                        else
                            $this.collapseNode(node);
                    });

        if(this.cfg.highlight && this.cfg.selectionMode) {
            this.jq.off('mouseenter.tree mouseleave.tree', nodeLabelSelector)
                        .on('mouseleave.tree', nodeLabelSelector, null, function() {
                            $(this).removeClass('ui-state-hover');
                        })
                        .on('mouseenter.tree', nodeLabelSelector, null, function() {
                            $(this).addClass('ui-state-hover');
                        });
        }

        if(this.isCheckboxSelection()) {
            var checkboxSelector = '.ui-chkbox-box:not(.ui-state-disabled)';

            this.jq.off('mouseleave.tree-checkbox mouseenter.tree-checkbox', checkboxSelector)
                        .on('mouseleave.tree-checkbox', checkboxSelector, null, function() {
                            $(this).removeClass('ui-state-hover');
                        })
                        .on('mouseenter.tree-checkbox', checkboxSelector, null, function() {
                            $(this).addClass('ui-state-hover');
                        });
        }

        this.jq.off('click.tree-content', nodeContentSelector)
                        .on('click.tree-content', nodeContentSelector, null, function(e) {
                            $this.nodeClick(e, $(this));
                        });

        if(this.cfg.filter) {
            this.filterInput = this.jq.find('.ui-tree-filter');
            PrimeFaces.skinInput(this.filterInput);

            this.filterInput.on('keydown.tree-filter', PrimeFaces.utils.blockEnterKey)
            .on('keyup.tree-filter', function(e) {
                if (PrimeFaces.utils.ignoreFilterKey(e)) {
                    return;
                }
                
                if($this.filterTimeout) {
                    clearTimeout($this.filterTimeout);
                }

                $this.filterTimeout = setTimeout(function() {
                    $this.filter();
                    $this.filterTimeout = null;
                }, 300);
            });
        }

        this.jq.on('scroll.tree', function(e) {
            $this.saveScrollState();
        });

        this.bindKeyEvents();
    },

    /**
     * Sets up all event listeners for keyboard interactions.
     * @private
     */
    bindKeyEvents: function() {
        var $this = this,
        pressTab = false;

        this.jq.on('mousedown.tree', function(e) {
            if($(e.target).is(':not(:input:enabled)')) {
                e.preventDefault();
            }
        })
        .on('focus.tree', function() {
            if(!$this.focusedNode && !pressTab) {
                $this.focusNode($this.getFirstNode());
            }
        });

        this.jq.off('keydown.tree blur.tree', '.ui-treenode-label').on('keydown.tree', '.ui-treenode-label', null, function(e) {
            if(!$this.focusedNode) {
                return;
            }

            var searchRowkey = "",
            keyCode = $.ui.keyCode;

            switch(e.which) {
                case keyCode.LEFT:
                    var rowkey = $this.focusedNode.data('rowkey').toString(),
                    keyLength = rowkey.length;

                    if($this.isExpanded($this.focusedNode)) {
                        $this.collapseNode($this.focusedNode);
                    }
                    else {
                        var nodeToFocus = null;
                        for(var i = 1; i < parseInt(keyLength / 2) + 1; i++){
                            searchRowkey = rowkey.substring(0, keyLength - 2 * i);
                            nodeToFocus = $this.container.find("li:visible[data-rowkey = '" + searchRowkey + "']");
                            if(nodeToFocus.length) {
                                $this.focusNode(nodeToFocus);
                                break;
                            }
                        }
                    }

                    e.preventDefault();
                break;

                case keyCode.RIGHT:
                    if(!$this.focusedNode.hasClass('ui-treenode-leaf')) {
                        var rowkey = $this.focusedNode.data('rowkey').toString(),
                        keyLength = rowkey.length;

                        if(!$this.isExpanded($this.focusedNode)) {
                            $this.expandNode($this.focusedNode);
                        }

                        if(!$this.isExpanded($this.focusedNode) && !$this.cfg.dynamic) {
                            searchRowkey = rowkey + '_0';
                            var nodeToFocus = $this.container.find("li:visible[data-rowkey = '" + searchRowkey + "']");

                            if(nodeToFocus.length) {
                                $this.focusNode(nodeToFocus);
                            }
                        }
                    }

                    e.preventDefault();
                break;

                case keyCode.UP:
                    var nodeToFocus = null,
                    prevNode = $this.focusedNode.prev();

                    if(prevNode.length) {
                        nodeToFocus = prevNode.find('li.ui-treenode:visible:last');
                        if(!nodeToFocus.length) {
                            nodeToFocus = prevNode;
                        }
                    }
                    else {
                        nodeToFocus = $this.focusedNode.closest('ul').parent('li');
                    }

                    if(nodeToFocus.length) {
                        $this.focusNode(nodeToFocus);
                    }

                    e.preventDefault();
                break;

                case keyCode.DOWN:
                    var nodeToFocus = null,
                    firstVisibleChildNode = $this.focusedNode.find("> ul > li:visible:first");

                    if(firstVisibleChildNode.length) {
                        nodeToFocus = firstVisibleChildNode;
                    }
                    else if($this.focusedNode.next().length) {
                        nodeToFocus = $this.focusedNode.next();
                    }
                    else {
                        var rowkey = $this.focusedNode.data('rowkey').toString();

                        if(rowkey.length !== 1) {
                            nodeToFocus = $this.searchDown($this.focusedNode);
                        }
                    }

                    if(nodeToFocus && nodeToFocus.length) {
                        $this.focusNode(nodeToFocus);
                    }

                    e.preventDefault();
                break;

                case keyCode.ENTER:
                case keyCode.SPACE:
                    if($this.cfg.selectionMode) {
                        var selectable = $this.focusedNode.children('.ui-treenode-content').hasClass('ui-tree-selectable');

                        if($this.cfg.onNodeClick) {
                            var retVal = $this.cfg.onNodeClick.call($this, $this.focusedNode, e);
                            if (retVal === false) {
                                return;
                            }
                        }

                        if(selectable) {
                            var selected = $this.isNodeSelected($this.focusedNode);

                            if($this.isCheckboxSelection()) {
                                $this.toggleCheckboxNode($this.focusedNode);
                            }
                            else {
                                if(selected) {
                                    $this.unselectNode($this.focusedNode);
                                }
                                else {
                                    if($this.isSingleSelection()) {
                                        $this.unselectAllNodes();
                                    }

                                    $this.selectNode($this.focusedNode);
                                    $this.cursorNode = $this.focusedNode;
                                }
                            }
                        }
                    }

                    e.preventDefault();
                break;

                case keyCode.TAB:
                    pressTab = true;
                    $this.jq.trigger('focus');
                    setTimeout(function() {
                        pressTab = false;
                    }, 2);

                break;
            }
        })
        .on('blur.tree', '.ui-treenode-label', null, function(e) {
            if($this.focusedNode) {
                $this.getNodeLabel($this.focusedNode).removeClass('ui-treenode-outline');
                $this.focusedNode = null;
            }
        });

        /* For copy/paste operation on drag and drop */
        $(document.body).on('keydown.tree', function(e) {
            $this.shiftKey = e.shiftKey;
        })
        .on('keyup.tree', function() {
            $this.shiftKey = false;
        });
    },

    /**
     * Searches for a node to focus, starting at the given node.
     * @private
     * @param {JQuery} node Node where to start the search.
     * @return {JQuery} A node to focus.
     */
    searchDown: function(node) {
        var nextOfParent = node.closest('ul').parent('li').next(),
        nodeToFocus = null;

        if(nextOfParent.length) {
            nodeToFocus = nextOfParent;
        }
        else if(node.hasClass('ui-treenode-leaf') && node.closest('ul').parent('li').length == 0){
            nodeToFocus = node;
        }
        else {
            var rowkey = node.data('rowkey').toString();

            if(rowkey.length !== 1) {
                nodeToFocus = this.searchDown(node.closest('ul').parent('li'));
            }
        }

        return nodeToFocus;
    },

    /**
     * Collapses 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 collapse. 
     */
    collapseNode: function(node) {
        var _self = this,
        nodeContent = node.find('> .ui-treenode-content'),
        toggleIcon = nodeContent.find('> .ui-tree-toggler'),
        nodeType = node.data('nodetype'),
        nodeIcon = toggleIcon.nextAll('span.ui-treenode-icon'),
        iconState = this.cfg.iconStates[nodeType],
        childrenContainer = node.children('.ui-treenode-children');

        //aria
        nodeContent.find('> .ui-treenode-label').attr('aria-expanded', false);

        toggleIcon.removeClass('ui-icon-triangle-1-s').addClass(_self.cfg.collapsedIcon);

        if(iconState) {
            nodeIcon.removeClass(iconState.expandedIcon).addClass(iconState.collapsedIcon);
        }

        if(this.cfg.animate) {
            childrenContainer.slideUp('fast', function() {
                _self.postCollapse(node, childrenContainer);
            });
        }
        else {
            childrenContainer.hide();
            this.postCollapse(node, childrenContainer);
        }
    },

    /**
     * Callback that is invoked after a node was collapsed.
     * @private
     * @param {JQuery} node The node that was collapsed.
     * @param {JQuery} childrenContainer The container element with the children of the collapsed node.
     */
    postCollapse: function(node, childrenContainer) {
        if(this.cfg.dynamic && !this.cfg.cache) {
            childrenContainer.empty();
        }

        if(!this.cfg.cache) {
            this.fireCollapseEvent(node);
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     * @return {JQuery}
     */
    getNodeChildrenContainer: function(node) {
        return node.children('.ui-treenode-children');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     */
    showNodeChildren: function(node) {
        var nodeContent = node.find('> .ui-treenode-content'),
        toggleIcon = nodeContent.find('> .ui-tree-toggler'),
        nodeType = node.data('nodetype'),
        nodeIcon = toggleIcon.nextAll('span.ui-treenode-icon'),
        iconState = this.cfg.iconStates[nodeType];

        //aria
        nodeContent.find('> .ui-treenode-label').attr('aria-expanded', true);

        toggleIcon.removeClass(this.cfg.collapsedIcon).addClass('ui-icon-triangle-1-s');

        if(iconState) {
            nodeIcon.removeClass(iconState.collapsedIcon).addClass(iconState.expandedIcon);
        }

        if(this.cfg.animate) {
            node.children('.ui-treenode-children').slideDown('fast');
        }
        else {
            node.children('.ui-treenode-children').show();
        }
    },

    /**
     * @override
     * @inheritdoc
     */
    unselectAllNodes: function() {
        this.selections = [];
        this.jq.find('.ui-treenode-label.ui-state-highlight').each(function() {
            $(this).removeClass('ui-state-highlight').closest('.ui-treenode').attr('aria-selected', false);
        });
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} node
     * @param {boolean} [silent]
     */
    selectNode: function(node, silent) {
        node.attr('aria-selected', true)
            .find('> .ui-treenode-content > .ui-treenode-label').addClass('ui-state-highlight');

        this.addToSelection(this.getRowKey(node));
        this.writeSelections();

        if(!silent)
            this.fireNodeSelectEvent(node);
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} node
     * @param {boolean} [silent]
     */
    unselectNode: function(node, silent) {
        var rowKey = this.getRowKey(node);

        node.attr('aria-selected', false).
            find('> .ui-treenode-content > .ui-treenode-label').removeClass('ui-state-highlight');

        this.removeFromSelection(rowKey);
        this.writeSelections();

        if(!silent)
            this.fireNodeUnselectEvent(node);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     */
    toggleCheckboxNode: function(node) {
        var $this = this,
        checkbox = node.find('> .ui-treenode-content > .ui-chkbox'),
        checked = checkbox.find('> .ui-chkbox-box > .ui-chkbox-icon').hasClass('ui-icon-check');

        this.toggleCheckboxState(checkbox, checked);

        if(this.cfg.propagateDown) {
            node.children('.ui-treenode-children').find('.ui-chkbox').each(function() {
                $this.toggleCheckboxState($(this), checked);
            });

            if(this.cfg.dynamic) {
                this.removeDescendantsFromSelection(node.data('rowkey'));
            }
        }

        if(this.cfg.propagateUp) {
            node.parents('li.ui-treenode-parent').each(function() {
                var parentNode = $(this),
                parentsCheckbox = parentNode.find('> .ui-treenode-content > .ui-chkbox'),
                children = parentNode.find('> .ui-treenode-children > .ui-treenode');

                if(checked) {
                    if(children.filter('.ui-treenode-unselected').length === children.length)
                        $this.uncheck(parentsCheckbox);
                    else
                        $this.partialCheck(parentsCheckbox);
                }
                else {
                    if(children.filter('.ui-treenode-selected').length === children.length)
                        $this.check(parentsCheckbox);
                    else
                        $this.partialCheck(parentsCheckbox);
                }
            });
        }

        this.writeSelections();

        if(checked)
            this.fireNodeUnselectEvent(node);
        else
            this.fireNodeSelectEvent(node);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    preselectCheckbox: function() {
        this.jq.find('.ui-chkbox-icon').not('.ui-icon-check').each(function() {
            var icon = $(this),
            node = icon.closest('li');

            if(node.children('.ui-treenode-children').find('.ui-chkbox-icon.ui-icon-check').length > 0) {
                node.addClass('ui-treenode-hasselected');
                icon.removeClass('ui-icon-blank').addClass('ui-icon-minus');
            }
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} checkbox
     */
    check: function(checkbox) {
        this._super(checkbox);
        checkbox.siblings('span.ui-treenode-label').addClass('ui-state-highlight');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} checkbox
     */
    uncheck: function(checkbox) {
        this._super(checkbox);
        checkbox.siblings('span.ui-treenode-label').removeClass('ui-state-highlight');
    },

    /**
     * Sets up the drag functionality.
     * @private
     */
    initDraggable: function() {
        this.makeDraggable(this.jq.find('span.ui-treenode-content'));
    },

    /**
     * Sets up the drop functionality.
     * @private
     */
    initDroppable: function() {
        this.makeDropPoints(this.jq.find('li.ui-tree-droppoint'));
        this.makeDropNodes(this.jq.find('span.ui-treenode-droppable'));
        this.initDropScrollers();
    },

    /**
     * Sets up the JQuery UI draggable for the given elements.
     * @private
     * @param {JQuery} elements A list of draggable nodes to set up. 
     */
    makeDraggable: function(elements) {
        var $this = this,
        dragdropScope = this.cfg.dragdropScope||this.id;

        elements.draggable({
            start: function(event, ui) {
                if(ui.helper) {
                    var element = $(event.target),
                    source = PF($(element.data('dragsourceid')).data('widget')),
                    height = 20;

                    if(source.cfg.multipleDrag && element.find('.ui-treenode-label').hasClass('ui-state-highlight')) {
                        source.draggedSourceKeys = $this.findSelectedParentKeys(source.selections.slice());
                        height = 20 * (source.draggedSourceKeys.length || 1);
                    }

                    $(ui.helper).height(height);
                }
            },
            helper: function() {
                var el = $('<div class="ui-tree-draghelper ui-state-highlight"></div>');
                el.width($this.jq.width());
                return el;
            },
            appendTo: document.body,
            zIndex: PrimeFaces.nextZindex(),
            revert: true,
            scope: dragdropScope,
            containment: 'document'
        })
        .data({
            'dragsourceid': this.jqId,
            'dragmode': this.cfg.dragMode
        });
    },

    /**
     * Sets up the JQuery UI drop points for the given elements.
     * @private
     * @param {JQuery} elements A list of drop points to set up. 
     */
    makeDropPoints: function(elements) {
        var $this = this,
        dragdropScope = this.cfg.dragdropScope||this.id;

        elements.droppable({
            hoverClass: 'ui-state-hover',
            accept: 'span.ui-treenode-content',
            tolerance: 'pointer',
            scope: dragdropScope,
            drop: function(event, ui) {
                var dragSource = PF($(ui.draggable.data('dragsourceid')).data('widget')),
                dropSource = $this,
                dropPoint = $(this),
                dropNode = dropPoint.closest('li.ui-treenode-parent'),
                dropNodeKey = $this.getRowKey(dropNode),
                transfer = (dragSource.id !== dropSource.id),
                draggedSourceKeys = dragSource.draggedSourceKeys,
                isDroppedNodeCopy = ($this.cfg.dropCopyNode && $this.shiftKey),
                draggedNodes,
                dragNodeKey;

                if(draggedSourceKeys) {
                    draggedNodes = dragSource.findNodes(draggedSourceKeys);
                }
                else {
                    draggedNodes = [ui.draggable];
                }

                if($this.cfg.controlled) {
                    $this.droppedNodeParams = [];
                }

                $this.invalidSourceKeys = [];

                for(var i = (draggedNodes.length - 1); i >= 0; i--) {
                    var draggedNode = $(draggedNodes[i]),
                    dragMode = ui.draggable.data('dragmode'),
                    dragNode = draggedNode.is('li.ui-treenode') ? draggedNode : draggedNode.closest('li.ui-treenode'),
                    dragNode = (isDroppedNodeCopy) ? dragNode.clone() : dragNode,
                    targetDragNode = $this.findTargetDragNode(dragNode, dragMode);

                    dragNodeKey = $this.getRowKey(targetDragNode);

                    if(!transfer && dropNodeKey && dropNodeKey.indexOf(dragNodeKey) === 0) {
                        return;
                    }

                    if($this.cfg.controlled) {
                        $this.droppedNodeParams.push({
                            'ui': ui,
                            'dragSource': dragSource,
                            'dragNode': dragNode,
                            'targetDragNode': targetDragNode,
                            'dropPoint': dropPoint,
                            'dropNode': dropNode,
                            'transfer': transfer
                        });
                    }
                    else {
                        $this.onDropPoint(ui, dragSource, dragNode, targetDragNode, dropPoint, dropNode, transfer);
                    }
                }

                if (!draggedSourceKeys) {
                    draggedSourceKeys = [dragNodeKey];
                }
                draggedSourceKeys = draggedSourceKeys.filter(function(key) {
                    return $.inArray(key, $this.invalidSourceKeys) === -1;
                });

                if (draggedSourceKeys && draggedSourceKeys.length) {
                    draggedSourceKeys = draggedSourceKeys.reverse().join(',');
                    $this.fireDragDropEvent({
                        'dragNodeKey': draggedSourceKeys,
                        'dropNodeKey': dropNodeKey,
                        'dragSource': dragSource.id,
                        'dndIndex': dropPoint.prevAll('li.ui-treenode').length,
                        'transfer': transfer,
                        'isDroppedNodeCopy': isDroppedNodeCopy
                    });
                }

                dragSource.draggedSourceKeys = null;
                $this.invalidSourceKeys = null;

                if(isDroppedNodeCopy) {
                    $this.initDraggable();
                }
            }
        });
    },

    /**
     * Callback for when a node was dropped on a drop point.
     * @private
     * @param {JQueryUI.DroppableOptions} ui Details about the drop event.
     * @param {PrimeFaces.widget.VerticalTree} dragSource Tree widget of the dragged node.
     * @param {JQuery} dragNode Node that was dragged.
     * @param {JQuery} targetDragNode Node that was the target of the drag. 
     * @param {JQuery} dropPoint The drop point where the node was dropped. 
     * @param {JQuery} dropNode The node on which the dragged node was dropped.
     * @param {boolean} transfer Whether a transfer should occur, i.e. whether the node was not dropped on itself.
     */
    onDropPoint: function(ui, dragSource, dragNode, targetDragNode, dropPoint, dropNode, transfer) {
        var dragNodeDropPoint = targetDragNode.next('li.ui-tree-droppoint'),
        oldParentNode = targetDragNode.parent().closest('li.ui-treenode-parent');

        ui.helper.remove();
        dropPoint.removeClass('ui-state-hover');

        var validDrop = this.validateDropPoint(dragNode, dropPoint);
        if(!validDrop) {
            if (this.invalidSourceKeys) {
                var dragNodeKey = this.getRowKey(targetDragNode);
                this.invalidSourceKeys.push(dragNodeKey);
            }
            return;
        }

        targetDragNode.hide().insertAfter(dropPoint);

        if(transfer) {
            if(dragSource.cfg.selectionMode) {
                dragSource.unselectSubtree(targetDragNode);
            }

            dragNodeDropPoint.remove();
            this.updateDragDropBindings(targetDragNode);
        }
        else {
            dragNodeDropPoint.insertAfter(targetDragNode);
        }

        if(oldParentNode.length && (oldParentNode.find('> ul.ui-treenode-children > li.ui-treenode').length === 0)) {
            this.makeLeaf(oldParentNode);
        }

        targetDragNode.fadeIn();

        if(this.isCheckboxSelection()) {
            this.syncDNDCheckboxes(dragSource, oldParentNode, dropNode);
        }

        this.syncDragDrop();
        if(transfer) {
            dragSource.syncDragDrop();
        }
    },

    /**
     * Sets up the JQuery UI dropables for the droppable nodes.
     * @private
     * @param {JQuery} elements List of elements to make droppable. 
     */
    makeDropNodes: function(elements) {
        var $this = this,
        dragdropScope = this.cfg.dragdropScope||this.id;

        elements.droppable({
            accept: '.ui-treenode-content',
            tolerance: 'pointer',
            scope: dragdropScope,
            over: function(event, ui) {
                $(this).children('.ui-treenode-label').addClass('ui-state-hover');
            },
            out: function(event, ui) {
                $(this).children('.ui-treenode-label').removeClass('ui-state-hover');
            },
            drop: function(event, ui) {
                var dragSource = PF($(ui.draggable.data('dragsourceid')).data('widget')),
                dropSource = $this,
                droppable = $(this),
                dropNode = droppable.closest('li.ui-treenode'),
                dropNodeKey = $this.getRowKey(dropNode),
                transfer = (dragSource.id !== dropSource.id),
                draggedSourceKeys = dragSource.draggedSourceKeys,
                isDroppedNodeCopy = ($this.cfg.dropCopyNode && $this.shiftKey),
                draggedNodes,
                dragNodeKey,
                dndIndex;

                if(draggedSourceKeys) {
                    draggedNodes = dragSource.findNodes(draggedSourceKeys);
                }
                else {
                    draggedNodes = [ui.draggable];
                }

                if($this.cfg.controlled) {
                    $this.droppedNodeParams = [];
                }

                $this.invalidSourceKeys = [];

                for(var i = 0; i < draggedNodes.length; i++) {
                    var draggedNode = $(draggedNodes[i]),
                    dragMode = ui.draggable.data('dragmode'),
                    dragNode = draggedNode.is('li.ui-treenode') ? draggedNode : draggedNode.closest('li.ui-treenode'),
                    dragNode = (isDroppedNodeCopy) ? dragNode.clone() : dragNode,
                    targetDragNode = $this.findTargetDragNode(dragNode, dragMode);

                    if(i === 0) {
                        dndIndex = dropNode.find('>.ui-treenode-children>li.ui-treenode').length;
                    }

                    dragNodeKey = $this.getRowKey(targetDragNode);

                    if(!transfer && dropNodeKey && dropNodeKey.indexOf(dragNodeKey) === 0) {
                        return;
                    }

                    if($this.cfg.controlled) {
                        $this.droppedNodeParams.push({
                            'ui': ui,
                            'dragSource': dragSource,
                            'dragNode': dragNode,
                            'targetDragNode': targetDragNode,
                            'droppable': droppable,
                            'dropNode': dropNode,
                            'transfer': transfer
                        });
                    }
                    else {
                        $this.onDropNode(ui, dragSource, dragNode, targetDragNode, droppable, dropNode, transfer);
                    }
                }

                if (!draggedSourceKeys) {
                    draggedSourceKeys = [dragNodeKey];
                }
                draggedSourceKeys = draggedSourceKeys.filter(function(key) {
                    return $.inArray(key, $this.invalidSourceKeys) === -1;
                });

                if (draggedSourceKeys && draggedSourceKeys.length) {
                    draggedSourceKeys = draggedSourceKeys.reverse().join(',');
                    $this.fireDragDropEvent({
                        'dragNodeKey': draggedSourceKeys,
                        'dropNodeKey': dropNodeKey,
                        'dragSource': dragSource.id,
                        'dndIndex': dndIndex,
                        'transfer': transfer,
                        'isDroppedNodeCopy': isDroppedNodeCopy
                    });
                }

                dragSource.draggedSourceKeys = null;
                $this.invalidSourceKeys = null;

                if(isDroppedNodeCopy) {
                    $this.initDraggable();
                }
            }
        });
    },

    /**
     * Callback for when a node was dropped.
     * @private
     * @param {JQueryUI.DroppableOptions} ui Details about the drop event.
     * @param {PrimeFaces.widget.VerticalTree} dragSource Tree widget of the dragged node.
     * @param {JQuery} dragNode Node that was dragged.
     * @param {JQuery} targetDragNode Node that was the target of the drag. 
     * @param {JQuery} droppable The jQUery UI droppable where the drop occurred. 
     * @param {JQuery} dropNode The node on which the dragged node was dropped.
     * @param {boolean} transfer Whether a transfer should occur.
     */
    onDropNode: function(ui, dragSource, dragNode, targetDragNode, droppable, dropNode, transfer) {
        var dragNodeDropPoint = targetDragNode.next('li.ui-tree-droppoint'),
        oldParentNode = targetDragNode.parent().closest('li.ui-treenode-parent'),
        childrenContainer = dropNode.children('.ui-treenode-children');

        ui.helper.remove();
        droppable.children('.ui-treenode-label').removeClass('ui-state-hover');

        var validDrop = this.validateDropNode(dragNode, dropNode, oldParentNode);
        if(!validDrop) {
            if (this.invalidSourceKeys) {
                var dragNodeKey = this.getRowKey(targetDragNode);
                this.invalidSourceKeys.push(dragNodeKey);
            }
            return;
        }

        if(childrenContainer.children('li.ui-treenode').length === 0) {
            this.makeParent(dropNode);
        }

        targetDragNode.hide();
        childrenContainer.append(targetDragNode);

        if(oldParentNode.length && (oldParentNode.find('> ul.ui-treenode-children > li.ui-treenode').length === 0)) {
            this.makeLeaf(oldParentNode);
        }

        if(transfer) {
            if(dragSource.cfg.selectionMode) {
                dragSource.unselectSubtree(targetDragNode);
            }

            dragNodeDropPoint.remove();
            this.updateDragDropBindings(targetDragNode);
        }
        else {
            childrenContainer.append(dragNodeDropPoint);
        }

        targetDragNode.fadeIn();

        if(this.isCheckboxSelection()) {
            this.syncDNDCheckboxes(dragSource, oldParentNode, dropNode);
        }

        this.syncDragDrop();
        if(transfer) {
            dragSource.syncDragDrop();
        }
    },

    /**
     * Filters the given array of row keys and removes child nodes of parent node in the array.
     * @private
     * @param {string[]} arr A list of row keys to check.
     * @return {string[]} A list of parent row keys.
     */
    findSelectedParentKeys: function(arr) {
        for(var i = 0; i < arr.length; i++) {
            var key = arr[i];
            for(var j = 0; j < arr.length && key !== -1; j++) {
                var tempKey = arr[j];
                if(tempKey !== -1 && key.length > tempKey.length && key.indexOf(tempKey) === 0) {
                    arr[i] = -1;
                }
            }
        }

        return arr.filter(function(item) {
            return item !== -1;
        });
    },

    /**
     * Initializes all drop scrollers of this tree.
     * @private
     */
    initDropScrollers: function() {
        var $this = this,
        dragdropScope = this.cfg.dragdropScope||this.id;

        this.jq.prepend('<div class="ui-tree-scroller ui-tree-scrollertop"></div>').append('<div class="ui-tree-scroller ui-tree-scrollerbottom"></div>');

        this.jq.children('div.ui-tree-scroller').droppable({
            accept: '.ui-treenode-content',
            tolerance: 'pointer',
            scope: dragdropScope,
            over: function() {
                var step = $(this).hasClass('ui-tree-scrollertop') ? -10 : 10;

                $this.scrollInterval = setInterval(function() {
                    $this.scroll(step);
                }, 100);
            },
            out: function() {
                clearInterval($this.scrollInterval);
            }
        });
    },

    /**
     * Scrolls this tree by the given amount.
     * @param {number} step Amount by which to scroll. 
     */
    scroll: function(step) {
        this.container.scrollTop(this.container.scrollTop() + step);
    },

    /**
     * Updates the drag&drop settings for the given node.
     * @private
     * @param {JQuery} node Node to update. 
     */
    updateDragDropBindings: function(node) {
        //self droppoint
        node.after('<li class="ui-tree-droppoint ui-droppable"></li>');
        this.makeDropPoints(node.next('li.ui-tree-droppoint'));

        //descendant droppoints
        var subtreeDropPoints = node.find('li.ui-tree-droppoint');
        if(subtreeDropPoints.hasClass('ui-droppable') && !this.shiftKey && !this.cfg.dropCopyNode) {
            subtreeDropPoints.droppable('destroy');
        }
        this.makeDropPoints(subtreeDropPoints);

        //descendant drop node contents
        var subtreeDropNodeContents = node.find('span.ui-treenode-content');
        if(subtreeDropNodeContents.hasClass('ui-droppable') && !this.shiftKey && !this.cfg.dropCopyNode) {
            subtreeDropNodeContents.droppable('destroy');
        }
        this.makeDropNodes(subtreeDropNodeContents);

        if(this.cfg.draggable) {
            subtreeDropNodeContents.data({
                'dragsourceid': this.jqId,
                'dragmode': this.cfg.dragMode
            });
        }
    },

    /**
     * Locates the target drag node, depending on the given drag mode.
     * @private
     * @param {JQuery} dragNode Node that was dragged.
     * @param {PrimeFaces.widget.BaseTree.DragMode} dragMode The current drag mode of this tree.
     * @return {JQuery} The resolved target drag node.
     */
    findTargetDragNode: function(dragNode, dragMode) {
        var targetDragNode = null;

        if(dragMode === 'self') {
            targetDragNode = dragNode;
        } else if(dragMode === 'parent') {
            targetDragNode = dragNode.parent().closest('li.ui-treenode');
        } else if(dragMode === 'ancestor') {
            targetDragNode = dragNode.parent().parents('li.ui-treenode:last');
        }

        if(targetDragNode.length === 0) {
            targetDragNode = dragNode;
        }

        return targetDragNode;
    },

    /**
     * Finds the nodes with the given row keys.
     * @param {string[]} rowkeys A list of row keys.
     * @return {JQuery[]} A list of nodes corresponding to the given row keys, in that order.
     */
    findNodes: function(rowkeys) {
        var nodes = [];
        for(var i = 0; i < rowkeys.length; i++) {
            nodes.push($(this.jqId + '\\:' + rowkeys[i]));
        }

        return nodes;
    },

    /**
     * Updates the row keys of all nodes.
     * @private
     */
    updateRowKeys: function() {
        var children = this.jq.find('> ul.ui-tree-container > li.ui-treenode');
        this.updateChildrenRowKeys(children, null);
    },

    /**
     * Updates the row keys of all given children.
     * @private
     * @param {JQuery} children List of children to update.
     * @param {string | null} rowkey Base prefix for the new rowkey.
     */
    updateChildrenRowKeys: function(children, rowkey) {
        var $this = this;

        children.each(function(i) {
            var childNode = $(this),
            oldRowKey = childNode.attr('data-rowkey'),
            newRowKey = (rowkey === null) ? i.toString() : rowkey + '_' + i;

            childNode.attr({
                'id': $this.id + ':' + newRowKey,
                'data-rowkey' : newRowKey
            });

            if(childNode.hasClass('ui-treenode-parent')) {
                $this.updateChildrenRowKeys(childNode.find('> ul.ui-treenode-children > li.ui-treenode'), newRowKey);
            }
        });
    },

    /**
     * After a drag&drop, validates if the drop is allowed.
     * @private
     * @param {JQuery} dragNode Node that was dragged.
     * @param {JQuery} dropPoint Element where the node was dropped.
     * @return {boolean} Whether the drop is allowed.
     */
    validateDropPoint: function(dragNode, dropPoint) {
        //dropped before or after
        if(dragNode.next().get(0) === dropPoint.get(0)||dragNode.prev().get(0) === dropPoint.get(0)) {
            return false;
        }

        //descendant of dropnode
        if(dragNode.has(dropPoint.get(0)).length) {
            return false;
        }

        //drop restriction
        if(this.cfg.dropRestrict) {
            if(this.cfg.dropRestrict === 'sibling' && dragNode.parent().get(0) !== dropPoint.parent().get(0)) {
                return false;
            }
        }

        return true;
    },

    /**
     * After a drag&drop, validates if the drop is allowed.
     * @private
     * @param {JQuery} dragNode Node that was dragged.
     * @param {JQuery} dropNode Node on which the dragged node was dropped.
     * @param {JQuery} oldParentNode Old parent of the dragged node.
     * @return {boolean} Whether the drop is allowed.
     */
    validateDropNode: function(dragNode, dropNode, oldParentNode) {
        //dropped on parent
        if(oldParentNode.get(0) === dropNode.get(0))
            return false;

        //descendant of dropnode
        if(dragNode.has(dropNode.get(0)).length) {
            return false;
        }

        //drop restriction
        if(this.cfg.dropRestrict) {
            if(this.cfg.dropRestrict === 'sibling') {
                return false;
            }
        }

        return true;
    },

    /**
     * Turns the given node into a leaf node.
     * @private
     * @param {JQuery} node A new leaf node to convert.
     */
    makeLeaf: function(node) {
        node.removeClass('ui-treenode-parent').addClass('ui-treenode-leaf');
        node.find('> .ui-treenode-content > .ui-tree-toggler').addClass('ui-treenode-leaf-icon').removeClass('ui-tree-toggler ui-icon ui-icon-triangle-1-s');
        node.children('.ui-treenode-children').hide().children().remove();
    },

    /**
     * Turns the given node into a parent node.
     * @private
     * @param {JQuery} node A new parent node to convert.
     */
    makeParent: function(node) {
        node.removeClass('ui-treenode-leaf').addClass('ui-treenode-parent');
        node.find('> span.ui-treenode-content > span.ui-treenode-leaf-icon').removeClass('ui-treenode-leaf-icon').addClass('ui-tree-toggler ui-icon ui-icon-triangle-1-e');
        node.children('.ui-treenode-children').append('<li class="ui-tree-droppoint ui-droppable"></li>');

        this.makeDropPoints(node.find('> ul.ui-treenode-children > li.ui-tree-droppoint'));
    },

    /**
     * Updates the tree after a drag&drop event.
     * @private
     */
    syncDragDrop: function() {
        var $this = this;

        if(this.cfg.selectionMode) {
            var selectedNodes = this.findNodes(this.selections);

            this.updateRowKeys();
            this.selections = [];
            $.each(selectedNodes, function(i, item) {
                $this.selections.push(item.attr('data-rowkey'));
            });
            this.writeSelections();
        }
        else {
            this.updateRowKeys();
        }
    },

    /**
     * Updates all checkboxes after a drag&drop.
     * @private
     * @param {PrimeFaces.widget.VerticalTree} dragSource Tree widget that is the source of the drag, when dragging
     * between two widgets.
     * @param {JQuery} oldParentNode Old node that was parent of the dropped node. 
     * @param {JQuery} newParentNode New node that is to be the parent of the dropped node.
     */
    syncDNDCheckboxes: function(dragSource, oldParentNode, newParentNode) {
        if(oldParentNode.length) {
            dragSource.propagateDNDCheckbox(oldParentNode);
        }

        if(newParentNode.length) {
            this.propagateDNDCheckbox(newParentNode);
        }
    },

    /**
     * Unselects the node and all child nodes.
     * @param {JQuery} node Node to unselect. 
     */
    unselectSubtree: function(node) {
        var $this = this;

        if(this.isCheckboxSelection()) {
            var checkbox = node.find('> .ui-treenode-content > .ui-chkbox');

            this.toggleCheckboxState(checkbox, true);

            node.children('.ui-treenode-children').find('.ui-chkbox').each(function() {
                $this.toggleCheckboxState($(this), true);
            });
        }
        else {
            node.find('.ui-treenode-label.ui-state-highlight').each(function() {
                $(this).removeClass('ui-state-highlight').closest('li.ui-treenode').attr('aria-selected', false);
            });
        }
    },

    /**
     * Updates the drag&drop checkboxes.
     * @private
     * @param {JQuery} node Node to which to limit the update.
     */
    propagateDNDCheckbox: function(node) {
        var checkbox = node.find('> .ui-treenode-content > .ui-chkbox'),
        children = node.find('> .ui-treenode-children > .ui-treenode');

        if(children.length) {
            if(children.filter('.ui-treenode-unselected').length === children.length)
                this.uncheck(checkbox);
            else if(children.filter('.ui-treenode-selected').length === children.length)
                this.check(checkbox);
            else
                this.partialCheck(checkbox);
        }

        var parent = node.parent().closest('.ui-treenode-parent');
        if(parent.length) {
            this.propagateDNDCheckbox(parent);
        }
    },

    /**
     * Callback for when a drag&drop occurred. Invokes the appropriate behaviors.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that triggered the drag&drop.
     */
    fireDragDropEvent: function(event) {
        var $this = this,
        options = {
            source: this.id,
            process: event.transfer ? this.id + ' ' + event.dragSource : this.id
        };

        options.params = [
            {name: this.id + '_dragdrop', value: true},
            {name: this.id + '_dragNode', value: event.dragNodeKey},
            {name: this.id + '_dragSource', value: event.dragSource},
            {name: this.id + '_dropNode', value: event.dropNodeKey},
            {name: this.id + '_dndIndex', value: event.dndIndex},
            {name: this.id + '_isDroppedNodeCopy', value: event.isDroppedNodeCopy}
        ];

        if(this.cfg.controlled) {
            options.oncomplete = function(xhr, status, args, data) {
                if(args.access) {
                    for(var i = 0; i < $this.droppedNodeParams.length; i++) {
                        var params = $this.droppedNodeParams[i];
                        if(params.dropPoint)
                            $this.onDropPoint(params.ui, params.dragSource, params.dragNode, params.targetDragNode, params.dropPoint, params.dropNode, params.transfer);
                        else
                            $this.onDropNode(params.ui, params.dragSource, params.dragNode, params.targetDragNode, params.droppable, params.dropNode, params.transfer);
                    }
                }
            };
        }

        if(this.hasBehavior('dragdrop')) {
            this.callBehavior('dragdrop', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * @override
     * @inheritdoc
     * @return {boolean}
     */
    isEmpty: function() {
        return (this.container.children().length === 0);
    },

    /**
     * Finds the first node.
     * @return {JQuery} The first node of this tree.
     */
    getFirstNode: function() {
        return this.jq.find('> ul.ui-tree-container > li:first-child');
    },

    /**
     * Finds the label element for the given node.
     * @param {JQuery} node Node for which to find the corresponding label.
     * @return {JQuery} The element with the label for the given node.
     */
    getNodeLabel: function(node) {
        return node.find('> span.ui-treenode-content > span.ui-treenode-label');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     */
    focusNode: function(node) {
        if(this.focusedNode) {
            this.getNodeLabel(this.focusedNode).removeClass('ui-treenode-outline');
        }

        this.getNodeLabel(node).addClass('ui-treenode-outline').trigger('focus');
        this.focusedNode = node;
    },

    /**
     * Applies the current filter value by sending an AJAX to the server.
     */
    filter: function() {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            global: false,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_filtering', value: true},
                     {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                    widget: $this,
                    handle: function(content) {
                        $this.container.html(content);
                    }
                });

                return true;
            },
            oncomplete: function() {
                if ($this.cfg.filterMode === 'contains') {
                    var notLeafNodes = $this.container.find('li.ui-treenode:not(.ui-treenode-leaf):visible');
                    for(var i = 0; i < notLeafNodes.length; i++) {
                        var node = notLeafNodes.eq(i),
                        hasChildNodes = node.children('.ui-treenode-children:empty').length;

                        if(hasChildNodes) {
                            node.removeClass('ui-treenode-parent').addClass('ui-treenode-leaf')
                                .find('> .ui-treenode-content > .ui-tree-toggler').removeClass('ui-tree-toggler ui-icon ui-icon-triangle-1-e').addClass('ui-treenode-leaf-icon');
                        }
                    }
                }
            }
        };

        if(this.hasBehavior('filter')) {
            this.callBehavior('filter', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }

    },

    /**
     * Reads the saved scroll position from the hidden input field and applies it.
     * @private
     */
    restoreScrollState: function() {
        var scrollState = this.scrollStateHolder.val(),
        scrollValues = scrollState.split(',');

        this.jq.scrollLeft(scrollValues[0]);
        this.jq.scrollTop(scrollValues[1]);
    },

    /**
     * Saves the current scroll position to the hidden input field.
     * @private
     */
    saveScrollState: function() {
        var scrollState = this.jq.scrollLeft() + ',' + this.jq.scrollTop();

        this.scrollStateHolder.val(scrollState);
    },

    /**
     * Resets the value of the hidden input field with the current scroll position.
     * @private
     */
    clearScrollState: function() {
        this.scrollStateHolder.val('0,0');
    }

});

/**
 * __PrimeFaces Horizontal Tree Widget__
 * 
 * Tree is used for displaying hierarchical data and creating a site navigation. This implements a horizontal tree.
 * 
 * @interface {PrimeFaces.widget.HorizontalTreeCfg} cfg The configuration for the
 * {@link  HorizontalTree| HorizontalTree 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.BaseTreeCfg} cfg
 */
PrimeFaces.widget.HorizontalTree = PrimeFaces.widget.BaseTree.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        if(PrimeFaces.env.isIE() && !this.cfg.disabled) {
            this.drawConnectors();
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    bindEvents: function() {
        var $this = this,
        selectionMode = this.cfg.selectionMode,
        togglerSelector = '.ui-tree-toggler',
        nodeContentSelector = '.ui-treenode-content.ui-tree-selectable';

        this.jq.off('click.tree-toggle', togglerSelector)
                    .on('click.tree-toggle', togglerSelector, null, function() {
                        var icon = $(this),
                        node = icon.closest('td.ui-treenode');

                        if(node.hasClass('ui-treenode-collapsed'))
                            $this.expandNode(node);
                        else
                            $this.collapseNode(node);
                    });

        if(selectionMode && this.cfg.highlight) {
            this.jq.off('mouseenter.tree mouseleave.tree', nodeContentSelector)
                        .on('mouseenter.tree', nodeContentSelector, null, function() {
                            $(this).addClass('ui-state-hover');
                        })
                        .on('mouseleave.tree', nodeContentSelector, null, function() {
                            $(this).removeClass('ui-state-hover');
                        });
        }

        if(this.isCheckboxSelection()) {
            var checkboxSelector = '.ui-chkbox-box:not(.ui-state-disabled)';

            this.jq.off('mouseleave.tree-checkbox mouseenter.tree-checkbox', checkboxSelector)
                        .on('mouseleave.tree-checkbox', checkboxSelector, null, function() {
                            $(this).removeClass('ui-state-hover');
                        })
                        .on('mouseenter.tree-checkbox', checkboxSelector, null, function() {
                            $(this).addClass('ui-state-hover');
                        });
        }

        this.jq.off('click.tree-content', nodeContentSelector)
                .on('click.tree-content', nodeContentSelector, null, function(e) {
                    $this.nodeClick(e, $(this));
                });

    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     */
    showNodeChildren: function(node) {
        node.attr('aria-expanded', true);

        var childrenContainer = node.next(),
        toggleIcon = node.find('> .ui-treenode-content > .ui-tree-toggler'),
        nodeType = node.data('nodetype'),
        iconState = this.cfg.iconStates[nodeType];

        if(iconState) {
            toggleIcon.nextAll('span.ui-treenode-icon').removeClass(iconState.collapsedIcon).addClass(iconState.expandedIcon);
        }

        toggleIcon.addClass('ui-icon-minus').removeClass('ui-icon-plus');
        node.removeClass('ui-treenode-collapsed');
        childrenContainer.show();

        if($.browser.msie) {
            this.drawConnectors();
        }
    },

    /**
     * Collapses 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 collapse. 
     */
    collapseNode: function(node) {
        var childrenContainer = node.next(),
        toggleIcon = node.find('> .ui-treenode-content > .ui-tree-toggler'),
        nodeType = node.data('nodetype'),
        iconState = this.cfg.iconStates[nodeType];

        if(iconState) {
            toggleIcon.nextAll('span.ui-treenode-icon').removeClass(iconState.expandedIcon).addClass(iconState.collapsedIcon);
        }

        toggleIcon.removeClass('ui-icon-minus').addClass('ui-icon-plus');
        node.addClass('ui-treenode-collapsed');
        childrenContainer.hide();

        if(this.cfg.dynamic && !this.cfg.cache) {
            childrenContainer.children('.ui-treenode-children').empty();
        }

        if(!this.cfg.cache) {
            this.fireCollapseEvent(node);
        }

        if($.browser.msie) {
            this.drawConnectors();
        }
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     * @return {JQuery}
     */
    getNodeChildrenContainer: function(node) {
        return node.next('.ui-treenode-children-container').children('.ui-treenode-children');
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} node
     * @param {boolean} [silent]
     */
    selectNode: function(node, silent) {
        node.removeClass('ui-treenode-unselected').addClass('ui-treenode-selected').children('.ui-treenode-content').addClass('ui-state-highlight');

        this.addToSelection(this.getRowKey(node));
        this.writeSelections();

        if(!silent)
            this.fireNodeSelectEvent(node);
    },

    /**
     * @override
     * @inheritdoc
     * @param {JQuery} node
     * @param {boolean} [silent]
     */
    unselectNode: function(node, silent) {
        var rowKey = this.getRowKey(node);

        node.removeClass('ui-treenode-selected').addClass('ui-treenode-unselected').children('.ui-treenode-content').removeClass('ui-state-highlight');

        this.removeFromSelection(rowKey);
        this.writeSelections();

        if(!silent)
            this.fireNodeUnselectEvent(node);
    },

    /**
     * @override
     * @inheritdoc
     */
    unselectAllNodes: function() {
        this.selections = [];
        this.jq.find('.ui-treenode-content.ui-state-highlight').each(function() {
            $(this).removeClass('ui-state-highlight').closest('.ui-treenode').attr('aria-selected', false);
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     */
    preselectCheckbox: function() {
        var _self = this;

        this.jq.find('.ui-chkbox-icon').not('.ui-icon-check').each(function() {
            var icon = $(this),
            node = icon.closest('.ui-treenode'),
            childrenContainer = _self.getNodeChildrenContainer(node);

            if(childrenContainer.find('.ui-chkbox-icon.ui-icon-check').length > 0) {
                icon.removeClass('ui-icon-blank').addClass('ui-icon-minus');
            }
        });
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} node
     */
    toggleCheckboxNode: function(node) {
        var $this = this,
        checkbox = node.find('> .ui-treenode-content > .ui-chkbox'),
        checked = checkbox.find('> .ui-chkbox-box > .ui-chkbox-icon').hasClass('ui-icon-check');

        this.toggleCheckboxState(checkbox, checked);

        if(this.cfg.propagateDown) {
            node.next('.ui-treenode-children-container').find('.ui-chkbox').each(function() {
                $this.toggleCheckboxState($(this), checked);
            });

            if(this.cfg.dynamic) {
                this.removeDescendantsFromSelection(node.data('rowkey'));
            }
        }

        if(this.cfg.propagateUp) {
            node.parents('td.ui-treenode-children-container').each(function() {
                var childrenContainer = $(this),
                parentNode = childrenContainer.prev('.ui-treenode-parent'),
                parentsCheckbox = parentNode.find('> .ui-treenode-content > .ui-chkbox'),
                children = childrenContainer.find('> .ui-treenode-children > table > tbody > tr > td.ui-treenode');

                if(checked) {
                    if(children.filter('.ui-treenode-unselected').length === children.length)
                        $this.uncheck(parentsCheckbox);
                    else
                        $this.partialCheck(parentsCheckbox);
                }
                else {
                    if(children.filter('.ui-treenode-selected').length === children.length)
                        $this.check(parentsCheckbox);
                    else
                        $this.partialCheck(parentsCheckbox);
                }
            });
        }

        this.writeSelections();

        if(checked)
            this.fireNodeUnselectEvent(node);
        else
            this.fireNodeSelectEvent(node);
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} checkbox
     */
    check: function(checkbox) {
        this._super(checkbox);
        checkbox.parent('.ui-treenode-content').addClass('ui-state-highlight');
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} checkbox
     */
    uncheck: function(checkbox) {
        this._super(checkbox);
        checkbox.parent('.ui-treenode-content').removeClass('ui-state-highlight');
    },

    /**
     * Draws the lines connection the tree nodes.
     * @private
     */
    drawConnectors: function() {
        this.jq.find('table.ui-treenode-connector-table').each(function() {
            var table = $(this),
            row = table.closest('tr');

            table.height(0).height(row.height());
        });
    },

    /**
     * @override
     * @inheritdoc
     * @return {boolean}
     */
    isEmpty: function() {
        return this.jq.children('table').length === 0;
    },

    /**
     * This implementation does nothing, focus is not supported in horizontal mode.
     * @override
     * @inheritdoc
     * @protected
     * @param {JQuery} node
     */
    focusNode: function(node) {
        //focus not supported in horizontal mode
    },

    /**
     * @override
     * @protected
     * @inheritdoc
     * @param {JQuery} checkbox
     */
    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');
        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);
     }

});
/**
 * __PrimeFaces TreeTable Widget__
 *
 * TreeTable is is used for displaying hierarchical data in tabular format.
 *
 * @typedef {"single" | "multiple" | "checkbox"} PrimeFaces.widget.TreeTable.SelectionMode Indicates how a row may be
 * selected.
 * - `single`: Only a single row may be selected at any time by clicking on it. Selecting another row will unselect the
 * currently selected row.
 * - `multiple`: Multiple rows can be selected via clicking while holding the ctrl or shift key.
 * - `checkbox`: One or more rows can be selected by clicking on the checkbox next to each row.
 *
 * @typedef {"ASCENDING" | "DESCENDING" | "UNSORTED"} PrimeFaces.widget.TreeTable.SortOrder The available sort order
 * types for the data table.
 *
 * @typedef {"eager" | "lazy"} PrimeFaces.widget.TreeTable.CellEditMode If cell editing mode is enabled, whether the
 * cell editors are loaded lazily.
 * - `eager`: Cell editors are loaded with the original page load or when the tree table is loaded.
 * - `lazy`: Cell editors are loaded via AJAX when inline editing is requested.
 *
 * @typedef {"row" | "cell"} PrimeFaces.widget.TreeTable.EditMode How the data in a tree table can be edited.
 * - `row`: A row is switched to edit mode and all cells can be edited at once.
 * - `cell`: An individual cell is switched to edit mode and can be edited.
 *
 * @typedef {"children" | "self"} PrimeFaces.widget.TreeTable.ExpandMode Defines which rows are expanded when the expand
 * icon next to a row is clicked.
 * - `self`: Only the row itself is expanded.
 * - `children`: The row and its children are expanded.

 * @implements {PrimeFaces.widget.ContextMenu.ContextMenuProvider<PrimeFaces.widget.TreeTable>}
 *
 * @prop {JQuery} bodyTable The DOM element for the main TABLE element.
 * @prop {JQuery} clone The DOM element for the  clone of the table head.
 * @prop {boolean} columnWidthsFixed Whether the width of all columns needs to stay fixed.
 * @prop {boolean} contextMenuClick Whether a click was performed on the context menu.
 * @prop {JQuery} currentCell The DOM element for the currently selected cell, when using inline editing.
 * @prop {JQuery} cursorNode The DOM element for the row at the cursor position, used for selecting multiple rows when
 * holding the shift key.
 * @prop {number} filterTimeout The set-timeout timer ID for the timer used for the delay during filtering.
 * @prop {JQuery} footerCols The DOM element for the TD columns in the table footer.
 * @prop {JQuery} footerTable The DOM element for the TABLE element of the footer.
 * @prop {JQuery} headerCols The DOM element for the TH columns in the header.
 * @prop {JQuery} headerTable The DOM element for the TABLE element of the header.
 * @prop {boolean} incellClick Whether a click was performed inside a cell.
 * @prop {JQuery} jqSelection The DOM element for the hidden input storing the selected rows.
 * @prop {string} marginRight CSS unit for the right margin of this tree table, determined from the scrollbar width.
 * @prop {PrimeFaces.widget.Paginator} paginator The paginator widget instance used for filtering.
 * @prop {boolean} percentageScrollHeight Whether the scroll height was specified in percent.
 * @prop {boolean} percentageScrollWidth Whether the scroll width was specified in percent.
 * @prop {number} relativeHeight The height of the visible scroll area relative to the total height of this tree table.
 * @prop {JQuery} resizableStateHolder INPUT element storing the current widths for each resizable column.
 * @prop {JQuery} resizerHelper The DOM element for the draggable handle for resizing columns.
 * @prop {JQuery} scrollBody The DOM element for the scrollable DIV with the body table.
 * @prop {JQuery} scrollFooter The DOM element for the scrollable DIV with the footer table.
 * @prop {JQuery} scrollFooterBox The DOM element for the container DIV of the footer table.
 * @prop {JQuery} scrollHeader The DOM element for the scrollable DIV with the header table.
 * @prop {JQuery} scrollHeaderBox The DOM element for the container DIV of the header table.
 * @prop {JQuery} scrollStateHolder The DOM element for the hidden input storing the current scroll position.
 * @prop {string} scrollStateVal The value of the {@link scrollStateHolder}.
 * @prop {string[]} selections A list of row keys of the currently selected rows.
 * @prop {JQuery} sortableColumns The DOM elements for the list of sortable columns.
 * @prop {PrimeFaces.widget.DataTable.SortMeta[]} sortMeta List of criteria by which to filter this table.
 * @prop {JQuery} stickyContainer The DOM element for the container with the sticky header.
 * @prop {JQuery} tbody The DOM element for the table body of this tree table.
 * @prop {JQuery} thead The DOM element for the table header of this tree table.
 * @prop {JQuery} theadClone The DOM element for the clone of the table header.
 *
 * @interface {PrimeFaces.widget.TreeTableCfg} cfg The configuration for the {@link  TreeTable| TreeTable 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.DeferredWidgetCfg} cfg
 *
 * @prop {boolean} cfg.allowUnsorting When true columns can be unsorted upon clicking sort.
 * @prop {PrimeFaces.widget.TreeTable.CellEditMode} cfg.cellEditMode Whether cell editors are loaded lazily.
 * @prop {string} cfg.cellSeparator Separator text to use in output mode of editable cells with multiple components.
 * @prop {boolean} cfg.disabledTextSelection Disables text selection on row click.
 * @prop {PrimeFaces.widget.TreeTable.EditMode} cfg.editMode If editing is enables and whether entire rows or individual
 * cells can be edited.
 * @prop {boolean} cfg.editable Whether data in this data table can be edited.
 * @prop {string} cfg.event Prefix of the event namespace used by the tree table.
 * @prop {PrimeFaces.widget.TreeTable.ExpandMode} cfg.expandMode Updates children only when set to `children` or the
 * node itself with children when set to `self` on node expand.
 * @prop {boolean} cfg.filter Whether filtering is enabled on this tree table.
 * @prop {number} cfg.filterDelay Delay in milliseconds the filtering.
 * @prop {string} cfg.filterEvent Event that trigger the tree table to be filtered.
 * @prop {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {boolean} cfg.liveResize Columns are resized live in this mode without using a resize helper.
 * @prop {boolean} cfg.multiSort Whether multi sort (filtering by multiple columns) is enabled.
 * @prop {boolean} cfg.nativeElements Whether native checkbox elements should be used for selection.
 * @prop {string} cfg.nodeType Type of the row nodes of this tree table.
 * @prop {Partial<PrimeFaces.widget.PaginatorCfg>} cfg.paginator When pagination is enabled: The paginator configuration
 * for the paginator.
 * @prop {boolean} cfg.resizableColumns Defines if columns can be resized or not.
 * @prop {number} cfg.scrollHeight Height of scrollable data.
 * @prop {number} cfg.scrollWidth Width of scrollable data.
 * @prop {boolean} cfg.scrollable Whether or not the data should be scrollable.
 * @prop {PrimeFaces.widget.TreeTable.SelectionMode} cfg.selectionMode How rows may be selected.
 * @prop {boolean} cfg.sorting `true` if sorting is enabled on the data table, `false` otherwise.
 * @prop {string[]} cfg.sortMetaOrder IDs of the columns by which to order. Order by the first column, then by the
 * second, etc.
 * @prop {boolean} cfg.stickyHeader Sticky header stays in window viewport during scrolling.
 * @prop {string} cfg.editInitEvent Event that triggers row/cell editing.
 * @prop {boolean} cfg.saveOnCellBlur Saves the changes in cell editing on blur, when set to false changes are
 * discarded.
 */
PrimeFaces.widget.TreeTable = PrimeFaces.widget.DeferredWidget.extend({

    /**
     * Map between the sort order names and the multiplier for the comparator.
     * @protected
     * @type {Record<PrimeFaces.widget.DataTable.SortOrder, -1 | 0 | 1>}
     */
    SORT_ORDER: {
        ASCENDING: 1,
        DESCENDING: -1,
        UNSORTED: 0
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.thead = $(this.jqId + '_head');
        this.tbody = $(this.jqId + '_data');
        this.cfg.expandMode = this.cfg.expandMode||"children";

        this.renderDeferred();
    },

    /**
     * @include
     * @override
     * @protected
     * @inheritdoc
     */
    _render: function() {
        if(this.cfg.scrollable) {
            this.setupScrolling();
        }

        if(this.cfg.filter) {
            this.setupFiltering();
        }

        if(this.cfg.resizableColumns) {
            this.resizableStateHolder = $(this.jqId + '_resizableColumnState');
            this.resizableState = [];

            if(this.resizableStateHolder.attr('value')) {
                this.resizableState = this.resizableStateHolder.val().split(',');
            }

            this.setupResizableColumns();
        }

        if(this.cfg.stickyHeader) {
            this.setupStickyHeader();
        }

        if(this.cfg.editable) {
            this.bindEditEvents();
        }

        this.bindEvents();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this.columnWidthsFixed = false;
        this.scrollStateVal = this.scrollStateHolder ? this.scrollStateHolder.val() : null;

        this._super(cfg);
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this,
        togglerSelector = '> tr > td:first-child > .ui-treetable-toggler';

        //expand and collapse
        this.tbody.off('click.treeTable-toggle', togglerSelector)
                    .on('click.treeTable-toggle', togglerSelector, null, function(e) {
                        var toggler = $(this),
                        node = toggler.closest('tr');

                        if(!node.data('processing')) {
                            node.data('processing', true);

                            if(toggler.hasClass('ui-icon-triangle-1-e'))
                                $this.expandNode(node);
                            else
                                $this.collapseNode(node);
                        }
                    });

        //selection
        if(this.cfg.selectionMode) {
            this.jqSelection = $(this.jqId + '_selection');
            var selectionValue = this.jqSelection.val();
            this.selections = selectionValue === "" ? [] : selectionValue.split(',');
            this.cfg.disabledTextSelection = this.cfg.disabledTextSelection === false ? false : true;

            this.bindSelectionEvents();
        }

        if(this.cfg.sorting) {
            this.bindSortEvents();
        }

        if(this.cfg.paginator) {
            this.cfg.paginator.paginate = function(newState) {
                $this.handlePagination(newState);
            };

            this.paginator = new PrimeFaces.widget.Paginator(this.cfg.paginator);
            this.paginator.bindSwipeEvents(this.jq, this.cfg);
        }
    },

    /**
     * Sets up all event listeners required for the standard filters. Also skins the filter inputs.
     * @private
     */
    setupFiltering: function() {
        var $this = this,
        filterColumns = this.thead.find('> tr > th.ui-filter-column');
        this.cfg.filterEvent = this.cfg.filterEvent||'keyup';
        this.cfg.filterDelay = this.cfg.filterDelay||300;

        filterColumns.children('.ui-column-filter').each(function() {
            var filter = $(this);

            if(filter.is('input:text')) {
                PrimeFaces.skinInput(filter);
                $this.bindTextFilter(filter);
            }
            else {
                PrimeFaces.skinSelect(filter);
                $this.bindChangeFilter(filter);
            }
        });
    },

    /**
     * Clear the filter input of this tree table and shows all rows again.
     */
    clearFilters: function() {
        this.thead.find('> tr > th.ui-filter-column > .ui-column-filter').val('');
        this.thead.find('> tr > th.ui-filter-column > .ui-column-customfilter :input').val('');
        $(this.jqId + '\\:globalFilter').val('');
        this.filter();
    },

    /**
     * Sets up the event listeners required for filtering this tree table, filtering either when enter is pressed or
     * when the {@link TreeTableCfg.filterEvent|configured event} occurs.
     * @private
     * @param {JQuery} filter The filter input field.
     */
    bindTextFilter: function(filter) {
        if(this.cfg.filterEvent === 'enter')
            this.bindEnterKeyFilter(filter);
        else
            this.bindFilterEvent(filter);
    },

    /**
     * Sets up the event listeners required for filtering this tree table when the filter input has changed.
     * @private
     * @param {JQuery} filter The filter input field.
     */
    bindChangeFilter: function(filter) {
        var $this = this;

        filter.on('change', function() {
            $this.filter();
        });
    },

    /**
     * Sets up the event listeners required for filtering this tree table when the enter key is pressed.
     * @private
     * @param {JQuery} filter The filter input field.
     */
    bindEnterKeyFilter: function(filter) {
        var $this = this;

        filter.on('keydown', PrimeFaces.utils.blockEnterKey)
        .on('keyup', function(e) {
            var key = e.which,
            keyCode = $.ui.keyCode;

            if(key === keyCode.ENTER) {
                $this.filter();

                e.preventDefault();
            }
        });
    },

    /**
     * Sets up the event listeners required for filtering this tree table.
     * @private
     * @param {JQuery} filter The filter input field.
     */
    bindFilterEvent: function(filter) {
        var $this = this;

        //prevent form submit on enter key
        filter.on('keydown.treeTable-blockenter', PrimeFaces.utils.blockEnterKey)
        .on(this.cfg.filterEvent + '.treeTable', function(e) {
            if($this.filterTimeout) {
                clearTimeout($this.filterTimeout);
            }

            $this.filterTimeout = setTimeout(function() {
                $this.filter();
                $this.filterTimeout = null;
            },
            $this.cfg.filterDelay);
        });
    },

    /**
     * Reads the current value of the filter input and performs a filtering operation. Sends an AJAX requests to the
     * server and updates this tree table with the result. Also invokes the appropriate behaviors.
     */
    filter: function() {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_filtering', value: true},
                     {name: this.id + '_encodeFeature', value: true}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.tbody.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                var paginator = $this.getPaginator();
                if(args && args.totalRecords) {
                    if(paginator) {
                        paginator.setTotalRecords(args.totalRecords);
                    }
                }
            }
        };

        if(this.hasBehavior('filter')) {
            this.callBehavior('filter', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Handles a pagination event by updating this tree table and invoking the appropriate behaviors.
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new pagination state to apply.
     */
    handlePagination: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            params: [
                {name: this.id + '_pagination', value: true},
                {name: this.id + '_first', value: newState.first},
                {name: this.id + '_rows', value: newState.rows}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.tbody.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.paginator.cfg.page = newState.page;
                $this.paginator.updateUI();
            }
        };

        if(this.hasBehavior('page')) {
            this.callBehavior('page', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Returns the paginator instance if any is exists.
     * @return {PrimeFaces.widget.Paginator | undefined} The paginator instance for this widget, or `undefined` if
     * paging is not enabled.
     */
    getPaginator: function() {
        return this.paginator;
    },

    /**
     * Sets up all events listeners required for selecting one or multiple rows of this tree table.
     * @private
     */
    bindSelectionEvents: function() {
        var $this = this,
        rowSelector = '> tr.ui-treetable-selectable-node';

        this.tbody.off('mouseenter.treeTable mouseleave.treeTable click.treeTable', rowSelector)
                    .on('mouseenter.treeTable', rowSelector, null, function(e) {
                        $(this).addClass('ui-state-hover');
                    })
                    .on('mouseleave.treeTable', rowSelector, null, function(e) {
                        $(this).removeClass('ui-state-hover');
                    })
                    .on('click.treeTable', rowSelector, null, function(e) {
                        $this.onRowClick(e, $(this));
                    });

        if(this.isCheckboxSelection()) {
           var checkboxSelector =  this.cfg.nativeElements ? '> tr.ui-treetable-selectable-node > td:first-child :checkbox':
                    '> tr.ui-treetable-selectable-node > td:first-child div.ui-chkbox-box';

                this.tbody.off('click.treeTable-checkbox mouseenter.treeTable-checkbox mouseleave.treeTable-checkbox', checkboxSelector)
                        .on('mouseenter.treeTable-checkbox', checkboxSelector, null, function(e) {
                            $(this).addClass('ui-state-hover');
                        })
                        .on('mouseleave.treeTable-checkbox', checkboxSelector, null, function(e) {
                            $(this).removeClass('ui-state-hover');
                        })
                        .on('click.treeTable-checkbox', checkboxSelector, null, function(e) {
                            var node = $(this).closest('tr.ui-treetable-selectable-node');
                            $this.toggleCheckboxNode(node);
                        });


                //initial partial selected visuals
                if(this.cfg.nativeElements) {
                    this.indeterminateNodes(this.tbody.children('tr.ui-treetable-partialselected'));
                }
        }
    },

    /**
     * Sets up all events listeners required for sorting the rows of this tree table.
     * @private
     */
    bindSortEvents: function() {
        var $this = this;

        this.cfg.multiSort = this.cfg.multiSort||false;
        this.cfg.allowUnsorting = this.cfg.allowUnsorting||false;
        this.sortMeta = [];

        this.sortableColumns = this.thead.find('> tr > th.ui-sortable-column');

        this.sortableColumns.each(function() {
            var columnHeader = $(this),
            columnHeaderId = columnHeader.attr('id'),
            sortIcon = columnHeader.children('span.ui-sortable-column-icon'),
            sortOrder = null;

            if (sortIcon.hasClass('ui-icon-triangle-1-n')) {
                sortOrder = $this.SORT_ORDER.ASCENDING;
                columnHeader.attr('aria-sort', 'ascending');
            }
            else if (sortIcon.hasClass('ui-icon-triangle-1-s')) {
                sortOrder = $this.SORT_ORDER.DESCENDING;
                columnHeader.attr('aria-sort', 'descending');
            }
            else {
                sortOrder = $this.SORT_ORDER.UNSORTED;
                columnHeader.attr('aria-sort', 'other');
            }

            columnHeader.data('sortorder', sortOrder);

            if ($this.cfg.multiSort && $this.cfg.sortMetaOrder) {
                var resolvedSortMetaIndex = $.inArray(columnHeaderId, $this.cfg.sortMetaOrder);

                $this.sortMeta[resolvedSortMetaIndex] = {
                    col: columnHeaderId,
                    order: sortOrder
                };
            }
        });

        this.sortableColumns.on('mouseenter.treeTable', function() {
            $(this).addClass('ui-state-hover');
        })
        .on('mouseleave.treeTable', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click.treeTable', function(e, metaKeyOn) {
            if(!$this.shouldSort(e, this)) {
                return;
            }

            PrimeFaces.clearSelection();

            var columnHeader = $(this),
                sortOrderData = columnHeader.data('sortorder'),
                sortOrder = (sortOrderData === $this.SORT_ORDER.UNSORTED) ? $this.SORT_ORDER.ASCENDING :
                    (sortOrderData === $this.SORT_ORDER.ASCENDING) ? $this.SORT_ORDER.DESCENDING :
                        $this.cfg.allowUnsorting ? $this.SORT_ORDER.UNSORTED : $this.SORT_ORDER.ASCENDING,
                metaKey = e.metaKey || e.ctrlKey || metaKeyOn;

            if (!$this.cfg.multiSort || !metaKey) {
                $this.sortMeta = [];
            }

            $this.addSortMeta({
                col: columnHeader.attr('id'),
                order: sortOrder
            });

            $this.sort(columnHeader, sortOrder, $this.cfg.multiSort && metaKey);
        });

        $this.updateSortPriorityIndicators();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.widget.ContextMenu} menuWidget
     * @param {PrimeFaces.widget.TreeTable} targetWidget
     * @param {string} targetId
     * @param {PrimeFaces.widget.ContextMenuCfg} cfg
     */
    bindContextMenu : function(menuWidget, targetWidget, targetId, cfg) {
        var targetSelector = targetId + ' .ui-treetable-data > ' + (cfg.nodeType ? 'tr.ui-treetable-selectable-node.' + cfg.nodeType : 'tr.ui-treetable-selectable-node');
        var targetEvent = cfg.event + '.treetable';

        $(document).off(targetEvent, targetSelector).on(targetEvent, targetSelector, null, function(e) {
            targetWidget.onRowRightClick(e, $(this));
            menuWidget.show(e);
        });
    },

    /**
     * Sets up the CSS and event listeners for the sticky header feature, if it is enabled.
     * @private
     */
    setupStickyHeader: function() {
        var table = this.thead.parent(),
        offset = table.offset(),
        win = $(window),
        $this = this;

        this.stickyContainer = $('<div class="ui-treetable ui-treetable-sticky ui-widget"><table></table></div>');
        this.clone = this.thead.clone(false);
        this.stickyContainer.children('table').append(this.thead);
        table.append(this.clone);

        this.stickyContainer.css({
            position: 'absolute',
            width: table.outerWidth() + 'px',
            top: offset.top + 'px',
            left: offset.left + 'px',
            'z-index': PrimeFaces.nextZindex()
        });

        this.jq.prepend(this.stickyContainer);

        if(this.cfg.resizableColumns) {
            this.relativeHeight = 0;
        }

        PrimeFaces.utils.registerScrollHandler(this, 'scroll.' + this.id + '_align', function() {
            var scrollTop = win.scrollTop(),
            tableOffset = table.offset();

            if(scrollTop > tableOffset.top) {
                $this.stickyContainer.css({
                                        'position': 'fixed',
                                        'top': '0px'
                                    })
                                    .addClass('ui-shadow ui-sticky');

                if($this.cfg.resizableColumns) {
                    $this.relativeHeight = scrollTop - tableOffset.top;
                }

                if(scrollTop >= (tableOffset.top + $this.tbody.height()))
                    $this.stickyContainer.hide();
                else
                    $this.stickyContainer.show();
            }
            else {
                $this.stickyContainer.css({
                                        'position': 'absolute',
                                        'top': tableOffset.top + 'px'
                                    })
                                    .removeClass('ui-shadow ui-sticky');

                if($this.stickyContainer.is(':hidden')) {
                    $this.stickyContainer.show();
                }

                if($this.cfg.resizableColumns) {
                    $this.relativeHeight = 0;
                }
            }
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.sticky-' + this.id + '_align', null, function() {
            $this.stickyContainer.width(table.outerWidth());
        });
    },

    /**
     * Sets up all events listeners required for editing entire rows or individual cells.
     * @private
     */
    bindEditEvents: function() {
        var $this = this;
        this.cfg.cellSeparator = this.cfg.cellSeparator||' ',
        this.cfg.saveOnCellBlur = (this.cfg.saveOnCellBlur === false) ? false : true;

        if(this.cfg.editMode === 'row') {
            var rowEditorSelector = '> tr > td > div.ui-row-editor';
            this.tbody.off('click.treetable', rowEditorSelector)
                        .on('click.treetable', rowEditorSelector, null, function(e) {
                            var element = $(e.target),
                            row = element.closest('tr');

                            if(element.hasClass('ui-icon-pencil')) {
                                $this.switchToRowEdit(row);
                                element.hide().siblings().show();
                            }
                            else if(element.hasClass('ui-icon-check')) {
                                $this.saveRowEdit(row);
                            }
                            else if(element.hasClass('ui-icon-close')) {
                                $this.cancelRowEdit(row);
                            }

                            e.preventDefault();
                        });
        }
        else if(this.cfg.editMode === 'cell') {
            var cellSelector = '> tr > td.ui-editable-column';
            var editEvent = (this.cfg.editInitEvent !== 'click') ? this.cfg.editInitEvent + '.treetable-cell click.treetable-cell' : 'click.treetable-cell';

            this.tbody.off(editEvent, cellSelector)
                .on(editEvent, cellSelector, null, function(e) {
                    if(!$(e.target).is('span.ui-treetable-toggler.ui-c')) {
                        $this.incellClick = true;

                        var item = $(this);
                        var cell = item.hasClass('ui-editable-column') ? item : item.closest('.ui-editable-column');

                        if(!cell.hasClass('ui-cell-editing') && e.type === $this.cfg.editInitEvent) {
                            $this.showCellEditor($(this));

                            if($this.cfg.editInitEvent === "dblclick") {
                                $this.incellClick = false;
                            }
                        }
                     }
                });

            $(document).off('click.treetable-cell-blur' + this.id)
                        .on('click.treetable-cell-blur' + this.id, function(e) {
                            var target = $(e.target);
                            if(!$this.incellClick && (target.is('.ui-input-overlay') || target.closest('.ui-input-overlay').length || target.closest('.ui-datepicker-buttonpane').length)) {
                                $this.incellClick = true;
                            }
                            
                            if(!$this.incellClick && $this.currentCell && !$this.contextMenuClick && !$.datepicker._datepickerShowing && $('.p-datepicker-panel:visible').length === 0) {
                                if($this.cfg.saveOnCellBlur)
                                    $this.saveCell($this.currentCell);
                                else
                                    $this.doCellEditCancelRequest($this.currentCell);
                            }
                            
                            $this.incellClick = false;
                            $this.contextMenuClick = false;
                        });
        }
    },

    /**
     * Sort this tree table by the given column, either in ascending or descending order.
     * @param {JQuery} columnHeader A column to sort by, must be a TH element of the THEAD.
     * @param {PrimeFaces.widget.TreeTable.SortOrder} order Whether to sort the rows in ascending or descending order.
     * @param {boolean} multi `true` if sorting by multiple columns is enabled, or `false` otherwise.
     */
    sort: function(columnHeader, order, multi) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [
                {name: this.id + '_sorting', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.tbody.html(content);

                            if(!multi) {
                                columnHeader.siblings().filter('.ui-state-active').removeData('sortorder').removeClass('ui-state-active')
                                                .find('.ui-sortable-column-icon').removeClass('ui-icon-triangle-1-n ui-icon-triangle-1-s');
                            }

                            columnHeader.addClass('ui-state-active').data('sortorder', order);
                            var sortIcon = columnHeader.find('.ui-sortable-column-icon'),
                            ariaLabel = columnHeader.attr('aria-label');

                            if (order === $this.SORT_ORDER.DESCENDING) {
                                sortIcon.removeClass('ui-icon-triangle-1-n').addClass('ui-icon-triangle-1-s');
                                columnHeader.attr('aria-sort', 'descending').attr('aria-label', $this.getSortMessage(ariaLabel, $this.otherMessage));
                                $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'descending')
                                    .attr('aria-label', $this.getSortMessage(ariaLabel, $this.otherMessage));
                            } else if (order === $this.SORT_ORDER.ASCENDING) {
                                sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-triangle-1-n');
                                columnHeader.attr('aria-sort', 'ascending').attr('aria-label', $this.getSortMessage(ariaLabel, $this.descMessage));
                                $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'ascending')
                                    .attr('aria-label', $this.getSortMessage(ariaLabel, $this.descMessage));
                            } else {
                                sortIcon.removeClass('ui-icon-triangle-1-s').addClass('ui-icon-carat-2-n-s');
                                columnHeader.removeClass('ui-state-active ').attr('aria-sort', 'other')
                                    .attr('aria-label', $this.getSortMessage(ariaLabel, $this.ascMessage));
                                $(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).attr('aria-sort', 'other')
                                    .attr('aria-label', $this.getSortMessage(ariaLabel, $this.ascMessage));
                            }

                            $this.updateSortPriorityIndicators();
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if($this.cfg.selectionMode && args.selection) {
                    $this.selections = args.selection.split(',');
                    $this.writeSelections();
                }
            }
        };

        options.params.push({name: this.id + '_sortKey', value: $this.joinSortMetaOption('col')});
        options.params.push({name: this.id + '_sortDir', value: $this.joinSortMetaOption('order')});

        if(this.hasBehavior('sort')) {
            this.callBehavior('sort', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Expands the given row of this tree table.
     * @param {JQuery} node A node to expand, must be a TR element.
     */
    expandNode: function(node) {
        var $this = this,
        nodeKey = node.attr('data-rk'),
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_expand', value: nodeKey}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            if($this.cfg.expandMode === "self")
                                node.replaceWith(content);
                            else
                                node.after(content);

                            node.find('.ui-treetable-toggler:first').addClass('ui-icon-triangle-1-s').removeClass('ui-icon-triangle-1-e');
                            node.attr('aria-expanded', true);
                            $this.indeterminateNodes($this.tbody.children('tr.ui-treetable-partialselected'));

                            if(this.cfg.scrollable) {
                                this.alignScrollBody();
                            }
                        }
                    });

                return true;
            },
            oncomplete: function() {
                node.data('processing', false);
                $this.updateVerticalScroll();
            }
        };

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

    /**
     * Collapses the given row of this tree table.
     * @param {JQuery} node A node to collapse, must be a TR element.
     */
    collapseNode: function(node) {
        var $this = this,
        nodeKey = node.attr('data-rk'),
        nextNodes = node.nextAll();

        for(var i = 0; i < nextNodes.length; i++) {
            var nextNode = nextNodes.eq(i),
            nextNodeRowKey = nextNode.attr('data-rk');

            if(nextNodeRowKey.indexOf(nodeKey) !== -1) {
               nextNode.remove();
            }
            else {
                break;
            }
        }

        node.attr('aria-expanded', false).find('.ui-treetable-toggler:first').addClass('ui-icon-triangle-1-e').removeClass('ui-icon-triangle-1-s');
        node.data('processing', false);

        if(this.cfg.scrollable) {
            this.alignScrollBody();
        }

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_collapse', value: nodeKey}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            // do nothing
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.updateVerticalScroll();
            }
        };

        if(this.hasBehavior('collapse')) {
            this.callBehavior('collapse', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Callback for when a row was clicked. Selects or unselects the row, if that feature is enabled.
     * @private
     * @param {JQuery.TriggeredEvent} event The click event that occurred.
     * @param {JQuery} node The node that was clicked.
     */
    onRowClick: function(event, node) {
        if($(event.target).is('td,span:not(.ui-c)')) {
            var selected = node.hasClass('ui-state-highlight'),
            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.selectNodesInRange(node);
                    }
                    else {
                        this.selectNode(node);
                        this.cursorNode = node;
                    }
                }
            }

            if(this.cfg.disabledTextSelection) {
                PrimeFaces.clearSelection();
            }
        }
    },

    /**
     * Callback for when a right click was performed on a node. Selects or unselects the row, if that feature is
     * enabled.
     * @private
     * @param {JQuery.TriggeredEvent} event The click event that occurred.
     * @param {JQuery} node The node that was clicked.
     */
    onRowRightClick: function(event, node) {
        var selected = node.hasClass('ui-state-highlight'),
            nodeKey = node.attr('data-rk');

        if(this.isCheckboxSelection()) {
            if(!selected) {
                this.toggleCheckboxNode(node);
            }
        }
        else {
            if(this.isSingleSelection() || !selected ) {
                this.unselectAllNodes();
            }
            this.selectNode(node);
        }

        this.fireSelectEvent(nodeKey, 'contextMenu');

        if(this.cfg.disabledTextSelection) {
            PrimeFaces.clearSelection();
        }
    },

    /**
     * Sends a select event on server side to invoke a select listener if defined.
     * @private
     * @param {string} nodeKey The key of the node that was selected.
     * @param {string} behaviorEvent Name of the event to fire.
     */
    fireSelectEvent: function(nodeKey, behaviorEvent) {
        if(this.hasBehavior(behaviorEvent)) {
            var ext = {
                    params: [{name: this.id + '_instantSelection', value: nodeKey}
                ]
            };

            this.callBehavior(behaviorEvent, ext);
        }
    },

    /**
     * Selects the given row. The {@link TreeTableCfg.selectionMode} must not be set to `checkbox`.
     * @param {JQuery} node A row to select, must be a TR element.
     * @param {boolean} [silent] If set to `true`, does not trigger event listeners.
     */
    selectNode: function(node, silent) {
        var nodeKey = node.attr('data-rk');

        node.removeClass('ui-treetable-partialselected').addClass('ui-state-highlight').attr('aria-selected', true);
        this.addToSelection(nodeKey);
        this.writeSelections();

        if(this.isCheckboxSelection()) {
            if(this.cfg.nativeElements)
                node.find('> td:first-child > :checkbox').prop('checked', true).prop('indeterminate', false).addClass('ui-state-active');
            else
                node.find('> td:first-child > div.ui-chkbox > div.ui-chkbox-box').addClass('ui-state-active').children('span.ui-chkbox-icon').removeClass('ui-icon-blank ui-icon-minus').addClass('ui-icon-check');
        }

        if(!silent) {
            this.fireSelectNodeEvent(nodeKey);
        }
    },

    /**
     * Unselects the given row. The {@link TreeTableCfg.selectionMode} must not be set to `checkbox`.
     * @param {JQuery} node A row to unselect, must be a TR element.
     * @param {boolean} [silent] If set to `true`, does not trigger event listeners.
     */
    unselectNode: function(node, silent) {
        var nodeKey = node.attr('data-rk');

        node.removeClass('ui-state-highlight ui-treetable-partialselected').attr('aria-selected', false);
        this.removeSelection(nodeKey);
        this.writeSelections();

        if(this.isCheckboxSelection()) {
            if(this.cfg.nativeElements)
                node.find('> td:first-child > :checkbox').prop('checked', false).prop('indeterminate', false).removeClass('ui-state-active');
            else
                node.find('> td:first-child > div.ui-chkbox > div.ui-chkbox-box').removeClass('ui-state-active').children('span.ui-chkbox-icon').addClass('ui-icon-blank').removeClass('ui-icon-check ui-icon-minus');
        }

        if(!silent) {
            this.fireUnselectNodeEvent(nodeKey);
        }
    },

    /**
     * Unselects all selected rows. The {@link TreeTableCfg.selectionMode} must not be set to `checkbox`.
     */
    unselectAllNodes: function() {
        var selectedNodes = this.tbody.children('tr.ui-state-highlight');
        for(var i = 0; i < selectedNodes.length; i++) {
            this.unselectNode(selectedNodes.eq(i), true);
        }

        this.selections = [];
        this.writeSelections();
    },

    /**
     * Selects all rows between the current row and the row that was just clicked. Used for multiple selections while
     * the shift key is pressed.
     * @private
     * @param {JQuery} node  A row that was just clicked.
     */
    selectNodesInRange: function(node) {
        if(this.cursorNode) {
            this.unselectAllNodes();

            var currentNodeIndex = node.index(),
            cursorNodeIndex = this.cursorNode.index(),
            startIndex = (currentNodeIndex > cursorNodeIndex) ? cursorNodeIndex : currentNodeIndex,
            endIndex = (currentNodeIndex > cursorNodeIndex) ? (currentNodeIndex + 1) : (cursorNodeIndex + 1),
            nodes = this.tbody.children();

            for(var i = startIndex ; i < endIndex; i++) {
                this.selectNode(nodes.eq(i), true);
            }
        }
        else {
            this.selectNode(node);
        }
    },

    /**
     * Sets the `indeterminate` attribute of the given rows to `true`.
     * @private
     * @param {JQuery} nodes List of rows to process.
     */
    indeterminateNodes: function(nodes) {
        for(var i = 0; i < nodes.length; i++) {
            nodes.eq(i).find('> td:first-child > :checkbox').prop('indeterminate', true);
        }
    },

    /**
     * When the {@link TreeTableCfg.selectionMode} is set to `checkbox`: select the given row if is is currently
     * unselected, or unselects it otherwise.
     * @param {JQuery} node A row to toggle, must be a TR element.
     */
    toggleCheckboxNode: function(node) {
        var selected = node.hasClass('ui-state-highlight'),
        rowKey = node.data('rk');

        //toggle itself
        if(selected)
            this.unselectNode(node, true);
        else
            this.selectNode(node, true);

        //propagate down
        var descendants = this.getDescendants(node);
        for(var i = 0; i < descendants.length; i++) {
            var descendant = descendants[i];
            if(selected)
                this.unselectNode(descendant, true);
            else
                this.selectNode(descendant, true);
        }

        if(selected) {
           this.removeDescendantsFromSelection(node.data('rk'));
        }

        //propagate up
        var parentNode = this.getParent(node);
        if(parentNode) {
            this.propagateUp(parentNode);
        }

        this.writeSelections();

        if(selected)
            this.fireUnselectNodeEvent(rowKey);
        else
            this.fireSelectNodeEvent(rowKey);
    },

    /**
     * Finds all descendants of the given row, i.e. all children, grandchildren etc.
     * @param {JQuery} node A node for which to get the descendants.
     * @return {JQuery} The descendants of the given row. An empty jQuery instance in case the row does not have
     * descendants.
     */
    getDescendants: function(node) {
        var nodeKey = node.attr('data-rk'),
        nextNodes = node.nextAll(),
        descendants = [];

        for(var i = 0; i < nextNodes.length; i++) {
            var nextNode = nextNodes.eq(i),
            nextNodeRowKey = nextNode.attr('data-rk');

            if(nextNodeRowKey.indexOf(nodeKey) != -1) {
                descendants.push(nextNode);
            }
            else {
                break;
            }
        }

        return descendants;
    },

    /**
     * Finds the children of the given row.
     * @param {JQuery} node A row for which to find the children.
     * @return {JQuery} The children of the given row. An empty jQuery instance in case the row does not have children.
     */
    getChildren: function(node) {
        var nodeKey = node.attr('data-rk'),
        nextNodes = node.nextAll(),
        children = [];

        for(var i = 0; i < nextNodes.length; i++) {
            var nextNode = nextNodes.eq(i),
            nextNodeParentKey = nextNode.attr('data-prk');

            if(nextNodeParentKey === nodeKey) {
                children.push(nextNode);
            }
        }

        return children;
    },

    /**
     * Propagates a select or unselect event up to the parents of the given row.
     * @private
     * @param {JQuery} node A node that was selected or unselected.
     */
    propagateUp: function(node) {
        var children = this.getChildren(node),
        allSelected = true,
        partialSelected = false,
        checkbox = this.cfg.nativeElements ? node.find('> td:first-child > :checkbox') :
                            node.find('> td:first-child > div.ui-chkbox > div.ui-chkbox-box > span.ui-chkbox-icon');

        for(var i = 0; i < children.length; i++) {
            var child = children[i],
            childSelected = child.hasClass('ui-state-highlight');

            allSelected = allSelected&&childSelected;
            partialSelected = partialSelected||childSelected||child.hasClass('ui-treetable-partialselected');
        }

        if(allSelected) {
            node.removeClass('ui-treetable-partialselected');
            this.selectNode(node, true);
        }
        else if(partialSelected) {
            node.removeClass('ui-state-highlight').addClass('ui-treetable-partialselected');

            if(this.cfg.nativeElements)
                checkbox.prop('indeterminate', true).removeClass('ui-state-active');
            else
                checkbox.removeClass('ui-icon-blank ui-icon-check').addClass('ui-icon-minus').closest('.ui-chkbox-box').removeClass('ui-state-active');

            this.removeSelection(node.attr('data-rk'));
        }
        else {
            node.removeClass('ui-state-highlight ui-treetable-partialselected');

            if(this.cfg.nativeElements)
                checkbox.prop('indeterminate', false).prop('checked', false).removeClass('ui-state-active');
            else
                checkbox.addClass('ui-icon-blank').removeClass('ui-icon-check ui-icon-minus').closest('.ui-chkbox-box').removeClass('ui-state-active');

            this.removeSelection(node.attr('data-rk'));
        }

        var parent = this.getParent(node);
        if(parent) {
            this.propagateUp(parent);
        }
    },

    /**
     * Finds the parent row of the given row of this tree table.
     * @param {JQuery} node A row for which to find the parent.
     * @return {JQuery | null} The parent of the given row, or `null` if it does not have a parent.
     */
    getParent: function(node) {
        var parent = $(this.jqId + '_node_' + node.attr('data-prk'));

        return parent.length === 1 ? parent : null;
    },

    /**
     * Removes all children of the given row from the list of currently selected rows.
     * @private
     * @param {string} rowKey A row with children that were unselected.
     */
    removeDescendantsFromSelection: function(rowKey) {
        this.selections = $.grep(this.selections, function(value) {
            return value.indexOf(rowKey + '_') !== 0;
        });
    },

    /**
     * Removes the given row from the list of currenlty selected rows.
     * @param {string} nodeKey A row that was unselected.
     */
    removeSelection: function(nodeKey) {
        this.selections = $.grep(this.selections, function(value) {
            return value !== nodeKey;
        });
    },

    /**
     * Adds the given row to the list of currently selected rows.
     * @private
     * @param {string} rowKey A row that was selected.
     */
    addToSelection: function(rowKey) {
        if(!this.isSelected(rowKey)) {
            this.selections.push(rowKey);
        }
    },

    /**
     * Checks whether the given row is currently selected.
     * @param {string} nodeKey Key of a row to check.
     * @return {boolean} Whether the given row is selected.
     */
    isSelected: function(nodeKey) {
        return PrimeFaces.inArray(this.selections, nodeKey);
    },

    /**
     * Checks whether only a single row of this tree table can be selected via clicking.
     * @return {boolean} `true` if the {@link TreeTableCfg.selectionMode} is set to `single`, or `false` otherwise.
     */
    isSingleSelection: function() {
        return this.cfg.selectionMode == 'single';
    },

    /**
     * Checks whether multiple rows of this tree table may be selected.
     * @return {boolean} `true` if the {@link TreeTableCfg.selectionMode} is set to `multiple`, or `false` otherwise.
     */
    isMultipleSelection: function() {
        return this.cfg.selectionMode == 'multiple';
    },

    /**
     * Checks whether rows of this tree table are selected via checkboxes.
     * @return {boolean} `true` if the {@link TreeTableCfg.selectionMode} is set to `checkbox`, or `false` otherwise.
     */
    isCheckboxSelection: function() {
        return this.cfg.selectionMode == 'checkbox';
    },

    /**
     * Saves the currently selected rows in the hidden input field.
     * @private
     */
    writeSelections: function() {
        this.jqSelection.val(this.selections.join(','));
    },

    /**
     * Callback for when a node was selected. Invokes the appropriate behaviors.
     * @private
     * @param {string} nodeKey Key of the row that was selected.
     */
    fireSelectNodeEvent: function(nodeKey) {
        if(this.isCheckboxSelection()) {
            var $this = this,
            options = {
                source: this.id,
                process: this.id
            };

            options.params = [
                {name: this.id + '_instantSelection', value: nodeKey}
            ];

            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 {
            this.fireSelectEvent(nodeKey, 'select');
        }
    },

    /**
     * Callback for when a node was unselected. Invokes the appropriate behaviors.
     * @private
     * @param {string} nodeKey Key of the row that was unselected.
     */
    fireUnselectNodeEvent: function(nodeKey) {
        if(this.hasBehavior('unselect')) {
            var ext = {
                params: [
                    {name: this.id + '_instantUnselection', value: nodeKey}
                ]
            };

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

    /**
     * Initializes scrolling and sets up the appropriate event handlers.
     * @private
     */
    setupScrolling: function() {
        this.scrollHeader = this.jq.children('div.ui-treetable-scrollable-header');
        this.scrollBody = this.jq.children('div.ui-treetable-scrollable-body');
        this.scrollFooter = this.jq.children('div.ui-treetable-scrollable-footer');
        this.scrollStateHolder = $(this.jqId + '_scrollState');
        this.scrollHeaderBox = this.scrollHeader.children('div.ui-treetable-scrollable-header-box');
        this.scrollFooterBox = this.scrollFooter.children('div.ui-treetable-scrollable-footer-box');
        this.headerTable = this.scrollHeaderBox.children('table');
        this.bodyTable = this.scrollBody.children('table');
        this.footerTable = this.scrollFooterBox.children('table');
        this.headerCols = this.headerTable.find('> thead > tr > th');
        this.footerCols = this.footerTable.find('> tfoot > tr > td');
        this.percentageScrollHeight = this.cfg.scrollHeight && (this.cfg.scrollHeight.indexOf('%') !== -1);
        this.percentageScrollWidth = this.cfg.scrollWidth && (this.cfg.scrollWidth.indexOf('%') !== -1);
        var $this = this;

        if(this.cfg.scrollHeight) {
            if(this.cfg.scrollHeight.indexOf('%') !== -1) {
                this.adjustScrollHeight();
            }

            if(this.cfg.scrollHeight.indexOf('vh') !== -1)  {
                this.applyViewPortScrollHeight();
            }

            this.marginRight = this.getScrollbarWidth() + 'px';
            this.scrollHeaderBox.css('margin-right', this.marginRight);
            this.scrollFooterBox.css('margin-right', this.marginRight);
            this.alignScrollBody();
        }

        this.fixColumnWidths();

        if(this.cfg.scrollWidth) {
            if(this.cfg.scrollWidth.indexOf('%') !== -1) {
                this.adjustScrollWidth();
            }
            else {
                this.setScrollWidth(parseInt(this.cfg.scrollWidth));
            }
        }

        this.cloneHead();

        this.restoreScrollState();

        this.updateVerticalScroll();

        this.scrollBody.on('scroll.treeTable', function() {
            var scrollLeft = $this.scrollBody.scrollLeft();
            $this.scrollHeaderBox.css('margin-left', -scrollLeft + 'px');
            $this.scrollFooterBox.css('margin-left', -scrollLeft + 'px');

            $this.saveScrollState();
        });

         this.scrollHeader.on('scroll.treeTable', function() {
            $this.scrollHeader.scrollLeft(0);
        });

        this.scrollFooter.on('scroll.treeTable', function() {
            $this.scrollFooter.scrollLeft(0);
        });

        PrimeFaces.utils.registerResizeHandler(this, 'resize.' + this.id + '_align', $this.jq, function() {
            if ($this.percentageScrollHeight) {
                $this.adjustScrollHeight();
            }
            if ($this.percentageScrollWidth) {
                $this.adjustScrollWidth();
            }
        });
    },

    /**
     * Makes a clone of the table header and adds it to the DOM.
     * @private
     */
    cloneHead: function() {
        this.theadClone = this.headerTable.children('thead').clone();
        this.theadClone.find('th').each(function() {
            var header = $(this);
            header.attr('id', header.attr('id') + '_clone');
        });
        this.theadClone.removeAttr('id').addClass('ui-treetable-scrollable-theadclone').height(0).prependTo(this.bodyTable);
    },

    /**
     * Applies the desired width to all columns.
     * @private
     */
     fixColumnWidths: function() {
        var $this = this;

        if(!this.columnWidthsFixed) {
            if(this.cfg.scrollable) {
                this.headerCols.each(function() {
                    var headerCol = $(this),
                    colIndex = headerCol.index(),
                    width = headerCol.width();

                    if ($this.resizableState) {
                        width = $this.findColWidthInResizableState(headerCol.attr('id')) || width;
                    }

                    headerCol.width(width);

                    if($this.footerCols.length > 0) {
                        var footerCol = $this.footerCols.eq(colIndex);
                        footerCol.width(width);
                    }
                });
            }
            else {
                var columns = this.jq.find('> table > thead > tr > th'),
                    visibleColumns = columns.filter(':visible'),
                    hiddenColumns = columns.filter(':hidden');

                this.setColumnsWidth(visibleColumns);
                /* IE fixes */
                this.setColumnsWidth(hiddenColumns);
            }

            this.columnWidthsFixed = true;
        }
    },

    /**
     * Applies the appropriated width to all given column elements.
     * @param {JQuery} columns A list of column elements.
     * @private
     */
    setColumnsWidth: function(columns) {
        if(columns.length) {
            var $this = this;

            columns.each(function() {
                var col = $(this),
                colStyle = col[0].style,
                width = colStyle.width||col.width();

                if ($this.resizableState) {
                    width = $this.findColWidthInResizableState(col.attr('id')) || width;
                }

                col.width(width);
            });
        }
    },

    /**
     * Computes and saves the resizable state of this data table, ie. which columns have got which width. May be used
     * later to restore the current column width after an AJAX update.
     * @private
     * @param {JQuery} columnHeader Element of a column header of this data table.
     * @param {JQuery} nextColumnHeader Element of the column header next to the given column header.
     * @param {JQuery} table The element for this data table.
     * @param {number} newWidth New width to be applied.
     * @param {number | null} nextColumnWidth Width of the column next to the given column header.
     */
    updateResizableState: function(columnHeader, nextColumnHeader, table, newWidth, nextColumnWidth) {
        var expandMode = (this.cfg.resizeMode === 'expand'),
        currentColumnId = columnHeader.attr('id'),
        nextColumnId = nextColumnHeader.attr('id'),
        tableId = this.id + "_tableWidthState",
        currentColumnState = currentColumnId + '_' + newWidth,
        nextColumnState = nextColumnId + '_' + nextColumnWidth,
        tableState = tableId + '_' + parseInt(table.css('width')),
        currentColumnMatch = false,
        nextColumnMatch = false,
        tableMatch = false;

        for(var i = 0; i < this.resizableState.length; i++) {
            var state = this.resizableState[i];
            if(state.indexOf(currentColumnId) === 0) {
                this.resizableState[i] = currentColumnState;
                currentColumnMatch = true;
            }
            else if(!expandMode && state.indexOf(nextColumnId) === 0) {
                this.resizableState[i] = nextColumnState;
                nextColumnMatch = true;
            }
            else if(expandMode && state.indexOf(tableId) === 0) {
                this.resizableState[i] = tableState;
                tableMatch = true;
            }
        }

        if(!currentColumnMatch) {
            this.resizableState.push(currentColumnState);
        }

        if(!expandMode && !nextColumnMatch) {
            this.resizableState.push(nextColumnState);
        }

        if(expandMode && !tableMatch) {
            this.resizableState.push(tableState);
        }

        this.resizableStateHolder.val(this.resizableState.join(','));
    },

    /**
     * Finds the saved width of the given column. The width of resizable columns may be saved to restore it after an
     * AJAX update.
     * @private
     * @param {string} id ID of a column
     * @return {string | null} The saved width of the given column in pixels. `null` when the given column does not
     * exist.
     */
    findColWidthInResizableState: function(id) {
        for (var i = 0; i < this.resizableState.length; i++) {
            var state = this.resizableState[i];
            if (state.indexOf(id) === 0) {
                return state.substring(state.lastIndexOf('_') + 1, state.length);
            }
        }

        return null;
    },

    /**
     * Adjust the view and scrolling position for the current height of the table.
     * @private
     */
    adjustScrollHeight: function() {
        var relativeHeight = this.jq.parent().innerHeight() * (parseInt(this.cfg.scrollHeight) / 100),
        tableHeaderHeight = this.jq.children('.ui-treetable-header').outerHeight(true),
        tableFooterHeight = this.jq.children('.ui-treetable-footer').outerHeight(true),
        scrollersHeight = (this.scrollHeader.outerHeight(true) + this.scrollFooter.outerHeight(true)),
        height = (relativeHeight - (scrollersHeight + tableHeaderHeight + tableFooterHeight));

        this.scrollBody.height(height);
    },

    /**
     * Sets the height of the scroll body to the value of this widget's configuration.
     * @private
     */
    applyViewPortScrollHeight: function() {
        this.scrollBody.height(this.cfg.scrollHeight);
    },

    /**
     * Adjust the view and scrolling position for the current width of the table.
     * @private
     */
    adjustScrollWidth: function() {
        var width = parseInt((this.jq.parent().innerWidth() * (parseInt(this.cfg.scrollWidth) / 100)));
        this.setScrollWidth(width);
    },

    /**
     * Applies the given outer width to an element.
     * @private
     * @param {JQuery} element An element to modify.
     * @param {number} width The new (outer) width for the element.
     */
    setOuterWidth: function(element, width) {
        var diff = element.outerWidth() - element.width();
        element.width(width - diff);
    },

    /**
     * Checks if there is any vertical overflow present currently.
     * @private
     * @return {boolean} `true` if there is overflow in the vertical y direction, or `false` otherwise.
     */
    hasVerticalOverflow: function() {
        return (this.cfg.scrollHeight && this.bodyTable.outerHeight() > this.scrollBody.outerHeight());
    },

    /**
     * Adjust the view for the given scrollbar width.
     * @private
     * @param {number} width The width of the scrollbar.
     */
    setScrollWidth: function(width) {
        var $this = this;
        this.jq.children('.ui-widget-header').each(function() {
            $this.setOuterWidth($(this), width);
        });
        this.scrollHeader.width(width);
        this.scrollBody.css('padding-right', '0px').width(width);
        this.scrollFooter.width(width);
    },

    /**
     * Aligns the scroll body element, taking into account the width of the scrollbar.
     * @private
     */
    alignScrollBody: function() {
        if(!this.cfg.scrollWidth) {
            if(this.hasVerticalOverflow())
                this.scrollBody.css('padding-right', '0px');
            else
                this.scrollBody.css('padding-right', this.getScrollbarWidth() + 'px');
        }
    },

    /**
     * Attempts to find a width for the scrollbar of the browser.
     * @private
     * @return {number} An estimate in pixels for the width of the native scrollbar.
     */
    getScrollbarWidth: function() {
        return $.browser.webkit ? '15' : PrimeFaces.calculateScrollbarWidth();
    },

    /**
     * Reads the scroll position from the hidden input element and applies it.
     * @private
     */
    restoreScrollState: function() {
        var scrollState = this.scrollStateVal||this.scrollStateHolder.val(),
        scrollValues = scrollState.split(',');

        this.scrollBody.scrollLeft(scrollValues[0]);
        this.scrollBody.scrollTop(scrollValues[1]);
        this.scrollStateVal = null;
    },

    /**
     * Stores the current scroll position in a hidden input element.
     * @private
     */
    saveScrollState: function() {
        var scrollState = this.scrollBody.scrollLeft() + ',' + this.scrollBody.scrollTop();

        this.scrollStateHolder.val(scrollState);
    },

    /**
     * Sets up the JQuery UI draggable with the appropriate event listeners for resizing columns.
     * @private
     */
    setupResizableColumns: function() {
        this.fixColumnWidths();

        if(!this.cfg.liveResize) {
            this.resizerHelper = $('<div class="ui-column-resizer-helper ui-state-highlight"></div>').appendTo(this.jq);
        }

        this.thead.find('> tr > th.ui-resizable-column:not(:last-child)').prepend('<span class="ui-column-resizer">&nbsp;</span>');
        var resizers = this.thead.find('> tr > th > span.ui-column-resizer'),
        $this = this;

        resizers.draggable({
            axis: 'x',
            start: function() {
                if($this.cfg.liveResize) {
                    $this.jq.css('cursor', 'col-resize');
                }
                else {
                    var header = $this.cfg.stickyHeader ? $this.clone : $this.thead,
                        height = $this.cfg.scrollable ? $this.scrollBody.height() : header.parent().height() - header.height() - 1;

                    if($this.cfg.stickyHeader) {
                        height = height - $this.relativeHeight;
                    }

                    $this.resizerHelper.height(height);
                    $this.resizerHelper.show();
                }
            },
            drag: function(event, ui) {
                if($this.cfg.liveResize) {
                    $this.resize(event, ui);
                }
                else {
                    $this.resizerHelper.offset({
                        left: ui.helper.offset().left + ui.helper.width() / 2,
                        top: $this.thead.offset().top + $this.thead.height()
                    });
                }
            },
            stop: function(event, ui) {
                var columnHeader = ui.helper.parent();
                ui.helper.css('left','');

                if($this.cfg.liveResize) {
                    $this.jq.css('cursor', 'default');
                } else {
                    $this.resize(event, ui);
                    $this.resizerHelper.hide();
                }

                var options = {
                    source: $this.id,
                    process: $this.id,
                    params: [
                        {name: $this.id + '_colResize', value: true},
                        {name: $this.id + '_columnId', value: columnHeader.attr('id')},
                        {name: $this.id + '_width', value: parseInt(columnHeader.width())},
                        {name: $this.id + '_height', value: parseInt(columnHeader.height())}
                    ]
                };

                if($this.hasBehavior('colResize')) {
                    $this.callBehavior('colResize', options);
                }

                if($this.cfg.stickyHeader) {
                    $this.reclone();
                }
            },
            containment: this.jq
        });
    },

    /**
     * Callback for when a row was resized. Adjust the column widths.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that triggered the resize.
     * @param {JQueryUI.DraggableEventUIParams} ui Details about the resize.
     */
    resize: function(event, ui) {
        var columnHeader = ui.helper.parent(),
            nextColumnHeader = columnHeader.next(),
            table = this.thead.parent(),
            change = null,
            newWidth = null,
            nextColumnWidth = null;

        if(this.cfg.liveResize) {
            change = columnHeader.outerWidth() - (event.pageX - columnHeader.offset().left),
            newWidth = (columnHeader.width() - change),
            nextColumnWidth = (nextColumnHeader.width() + change);
        }
        else {
            change = (ui.position.left - ui.originalPosition.left),
            newWidth = (columnHeader.width() + change),
            nextColumnWidth = (nextColumnHeader.width() - change);
        }

        if(newWidth > 15 && nextColumnWidth > 15) {
            columnHeader.width(newWidth);
            nextColumnHeader.width(nextColumnWidth);
            this.updateResizableState(columnHeader, nextColumnHeader, table, newWidth, nextColumnWidth);

            var colIndex = columnHeader.index();

            if(this.cfg.scrollable) {
                this.theadClone.find(PrimeFaces.escapeClientId(columnHeader.attr('id') + '_clone')).width(newWidth);
                this.theadClone.find(PrimeFaces.escapeClientId(nextColumnHeader.attr('id') + '_clone')).width(nextColumnWidth);

                if(this.footerCols.length > 0) {
                    var footerCol = this.footerCols.eq(colIndex),
                    nextFooterCol = footerCol.next();

                    footerCol.width(newWidth);
                    nextFooterCol.width(nextColumnWidth);
                }
            }
        }
    },

    /**
     * Removes the cloned table header and create a new clone.
     * @private
     */
    reclone: function() {
        this.clone.remove();
        this.clone = this.thead.clone(false);
        this.jq.children('table').append(this.clone);
    },

    /**
     * Switches a row to edit mode and displays the editors for that row.
     * @param {JQuery} row A row for which to activate the editors. Must be a TR element.
     */
    switchToRowEdit: function(row) {
        this.showRowEditors(row);

        if(this.hasBehavior('rowEditInit')) {
            var rowIndex = row.data('rk');

            var ext = {
                params: [{name: this.id + '_rowEditIndex', value: rowIndex}]
            };

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

    /**
     * Hides the row and display the row editors.
     * @private
     * @param {JQuery} row A row for which to show the editors.
     */
    showRowEditors: function(row) {
        row.addClass('ui-state-highlight ui-row-editing').children('td.ui-editable-column').each(function() {
            var column = $(this);

            column.find('.ui-cell-editor-output').hide();
            column.find('.ui-cell-editor-input').show();
        });
    },

    /**
     * When a row is currently being edited: Saves the edited row and hides the editors.
     * @param {JQuery} rowEditor A row to save, must be a TR element.
     */
    saveRowEdit: function(rowEditor) {
        this.doRowEditRequest(rowEditor, 'save');
    },

    /**
     * When a row is currently being edited: cancels row editing and discards the entered data.
     * @param {JQuery} rowEditor A row for which to cancel editing, must be a TR element.
     */
    cancelRowEdit: function(rowEditor) {
        this.doRowEditRequest(rowEditor, 'cancel');
    },

    /**
     * Sends an AJAX request to the server to handle a row save or cancel event.
     * @private
     * @param {JQuery} rowEditor The inline editor with data that needs to be saved or discarded.
     * @param {string} action The action to perform, either `save` or `cancel`.
     */
    doRowEditRequest: function(rowEditor, action) {
        var row = rowEditor.closest('tr'),
        rowIndex = row.data('rk'),
        expanded = row.hasClass('ui-expanded-row'),
        $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.getParentFormId(),
            params: [{name: this.id + '_rowEditIndex', value: rowIndex},
                     {name: this.id + '_rowEditAction', value: action}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            if(expanded) {
                                this.collapseRow(row);
                            }

                            this.updateRows(row, content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(args && args.validationFailed) {
                    $this.invalidateRow(rowIndex);
                }
            }
        };

        if(action === 'save') {
            this.getRowEditors(row).each(function() {
                options.params.push({name: this.id, value: this.id});
            });
        }

        if(action === 'save' && this.hasBehavior('rowEdit')) {
            this.callBehavior('rowEdit', options);
        }
        else if(action === 'cancel' && this.hasBehavior('rowEditCancel')) {
            this.callBehavior('rowEditCancel', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Updates a row with the given HTML content.
     * @private
     * @param {JQuery} row A row to update.
     * @param {string | HTMLElement | HTMLElement[] | JQuery} content The new HTML content of the row.
     */
    updateRows: function(row, content) {
        this.tbody.children('tr').filter('[data-prk^="'+ row.data('rk') +'"]').remove();
        row.replaceWith(content);
    },

    /**
     * Callback for when validation did not succeed. Switches all editors of the given row to the error state.
     * @private
     * @param {number} index 0-based index of the row with cell editors.
     */
    invalidateRow: function(index) {
        this.tbody.children('tr').eq(index).addClass('ui-widget-content ui-row-editing ui-state-error');
    },

    /**
     * Finds all editors of a row.
     * @private
     * @param {JQuery} row A row for which to find all cell editors.
     * @return {JQuery} All cell editors of the given row.
     */
    getRowEditors: function(row) {
        return row.find('div.ui-cell-editor');
    },

    /**
     * Collapses the given row of this tree table after saving the contents of an inline editor.
     * @private
     * @param {JQuery} row A row to collapse.
     */
    collapseRow: function(row) {
        row.removeClass('ui-expanded-row').next('.ui-expanded-row-content').remove();
    },

    /**
     * Activates the inline editor for the given cell.
     * @param {JQuery} c The cell TD element for which to activate the inline editor.
     */
    showCellEditor: function(c) {
        this.incellClick = true;

        var cell = null;

        if(c) {
            cell = c;

            //remove contextmenu selection highlight
            if(this.contextMenuCell) {
                this.contextMenuCell.parent().removeClass('ui-state-highlight');
            }
        }
        else {
            cell = this.contextMenuCell;
        }

        var editorInput = cell.find('> .ui-cell-editor > .ui-cell-editor-input');
        if(editorInput.length !== 0 && editorInput.children().length === 0 && this.cfg.editMode === 'cell') {
            // for lazy cellEditMode
            this.cellEditInit(cell);
        }
        else {
            this.showCurrentCell(cell);

            if(this.hasBehavior('cellEditInit')) {
                var cellInfo = this.getCellMeta(cell);
                var ext = {
                    params: [{name: this.id + '_cellInfo', value: cellInfo}]
                };
                this.callBehavior('cellEditInit', ext);
            }
        }
    },

    /**
     * Makes the inline cell visible and sets up the appropriate event listeners.
     * @private
     * @param {JQuery} cell The cell TD element for which to activate inline editing mode.
     */
    showCurrentCell: function(cell) {
        var $this = this;

        if(this.currentCell) {
            if(this.cfg.saveOnCellBlur)
                this.saveCell(this.currentCell);
            else if(!this.currentCell.is(cell))
                this.doCellEditCancelRequest(this.currentCell);
        }

        this.currentCell = cell;

        var cellEditor = cell.children('div.ui-cell-editor'),
        displayContainer = cellEditor.children('div.ui-cell-editor-output'),
        inputContainer = cellEditor.children('div.ui-cell-editor-input'),
        inputs = inputContainer.find(':input:enabled'),
        multi = inputs.length > 1;

        cell.addClass('ui-state-highlight ui-cell-editing');
        displayContainer.hide();
        inputContainer.show();
        inputs.eq(0).trigger('focus').trigger('select');

        //metadata
        if(multi) {
            var oldValues = [];
            for(var i = 0; i < inputs.length; i++) {
                oldValues.push(inputs.eq(i).val());
            }

            cell.data('multi-edit', true);
            cell.data('old-value', oldValues);
        }
        else {
            cell.data('multi-edit', false);
            cell.data('old-value', inputs.eq(0).val());
        }

        //bind events on demand
        if(!cell.data('edit-events-bound')) {
            cell.data('edit-events-bound', true);

            inputs.on('keydown.treetable-cell', function(e) {
                    var keyCode = $.ui.keyCode,
                    shiftKey = e.shiftKey,
                    key = e.which,
                    input = $(this);

                    if(key === keyCode.ENTER) {
                        $this.saveCell(cell);

                        e.preventDefault();
                    }
                    else if(key === keyCode.TAB) {
                        if(multi) {
                            var focusIndex = shiftKey ? input.index() - 1 : input.index() + 1;

                            if(focusIndex < 0 || (focusIndex === inputs.length)) {
                                $this.tabCell(cell, !shiftKey);
                            } else {
                                inputs.eq(focusIndex).trigger('focus');
                            }
                        }
                        else {
                            $this.tabCell(cell, !shiftKey);
                        }

                        e.preventDefault();
                    }
                    else if(key === keyCode.ESCAPE) {
                        $this.doCellEditCancelRequest(cell);
                        e.preventDefault();
                    }
                })
                .on('focus.treetable-cell click.treetable-cell', function(e) {
                    $this.currentCell = cell;
                });
        }
    },

    /**
     * Callback for when the tab key is pressed, switches (focuses) to the next or previous cell editor.
     * @private
     * @param {JQuery} cell The currently focused cell.
     * @param {boolean} forward `true` to move to the next cell, or `false` to move to the previous cell.
     */
    tabCell: function(cell, forward) {
        var targetCell = forward ? cell.nextAll('td.ui-editable-column:first') : cell.prevAll('td.ui-editable-column:first');
        if(targetCell.length == 0) {
            var tabRow = forward ? cell.parent().next() : cell.parent().prev();
            targetCell = forward ? tabRow.children('td.ui-editable-column:first') : tabRow.children('td.ui-editable-column:last');
        }

        this.showCellEditor(targetCell);
    },

    /**
     * Saves the current data entered into a cell's inline editor. Checks whether the data has changed and if so, sends
     * it to the server.
     * @param {JQuery} cell A cell with an inline editor to save.
     */
    saveCell: function(cell) {
        var inputs = cell.find('div.ui-cell-editor-input :input:enabled'),
        changed = false,
        $this = this;

        if(cell.data('multi-edit')) {
            var oldValues = cell.data('old-value');
            for(var i = 0; i < inputs.length; i++) {
                if(inputs.eq(i).val() != oldValues[i]) {
                    changed = true;
                    break;
                }
            }
        }
        else {
            changed = (inputs.eq(0).val() != cell.data('old-value'));
        }

        if(changed)
            $this.doCellEditRequest(cell);
        else
            $this.viewMode(cell);

        if(this.cfg.saveOnCellBlur) {
            this.currentCell = null;
        }
    },

    /**
     * Switch from edit mode to view mode, Hides the inline editor and displays the data.
     * @private
     * @param {JQuery} cell The cell with an activate inline editor to hide.
     */
    viewMode: function(cell) {
        var cellEditor = cell.children('div.ui-cell-editor'),
        editableContainer = cellEditor.children('div.ui-cell-editor-input'),
        displayContainer = cellEditor.children('div.ui-cell-editor-output');

        cell.removeClass('ui-cell-editing ui-state-error ui-state-highlight');
        displayContainer.show();
        editableContainer.hide();
        cell.removeData('old-value').removeData('multi-edit');

        if(this.cfg.cellEditMode === "lazy") {
            editableContainer.children().remove();
        }
    },

    /**
     * When the inline editor a cell is active and the user wants to save the changes: send the newly entered data to
     * the server and hide the editor.
     * @private
     * @param {JQuery} cell The cell with an inline editor to be saved.
     */
    doCellEditRequest: function(cell) {
        var cellEditor = cell.children('.ui-cell-editor'),
        cellEditorId = cellEditor.attr('id'),
        cellInfo = this.getCellMeta(cell),
        $this = this;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [{name: this.id + '_cellInfo', value: cellInfo},
                     {name: cellEditorId, value: cellEditorId}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-output').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                if(args.validationFailed)
                    cell.addClass('ui-state-error');
                else
                    $this.viewMode(cell);
            }
        };

        if(this.hasBehavior('cellEdit')) {
            this.callBehavior('cellEdit', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When the inline editor a cell is active and the user requests a cancel: discards the data and loads the
     * original content of the cell.
     * @private
     * @param {JQuery} cell The cell for which editing should be canceled.
     */
    doCellEditCancelRequest: function(cell) {
        var cellEditor = cell.children('.ui-cell-editor'),
        cellIndex = cell.index(),
        cellInfo = cell.closest('tr').data('rk') + ',' + cellIndex,
        $this = this;

        this.currentCell = null;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [{name: this.id + '_cellEditCancel', value: true},
                     {name: this.id + '_cellInfo', value: cellInfo}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-input').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                $this.viewMode(cell);
                cell.data('edit-events-bound', false);
            }
        };

        if(this.hasBehavior('cellEditCancel')) {
            this.callBehavior('cellEditCancel', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * When activating the inline editor of a cell, starts an AJAX request to fetch the editor's HTML. Also invokes
     * the appropriate behaviors.
     * @private
     * @param {JQuery} cell The cell for which inline editing should be activated.
     */
    cellEditInit: function(cell) {
        var cellEditor = cell.children('.ui-cell-editor'),
        cellInfo = this.getCellMeta(cell),
        $this = this;

        var options = {
            source: this.id,
            process: this.id,
            update: this.id,
            global: false,
            params: [{name: this.id + '_cellEditInit', value: true},
                     {name: this.id + '_cellInfo', value: cellInfo}],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            cellEditor.children('.ui-cell-editor-input').html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                cell.data('edit-events-bound', false);
                $this.showCurrentCell(cell);
            }
        };

        if(this.hasBehavior('cellEditInit')) {
            this.callBehavior('cellEditInit', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Retrieves the meta data of the given cell.
     * @private
     * @param {JQuery} cell A cell to inspect.
     * @return {string} The meta data for the given cell.
     */
    getCellMeta: function(cell) {
        var cellIndex = cell.index(),
            cellInfo = cell.closest('tr').data('rk') + ',' + cellIndex;
        return cellInfo;
    },

    /**
     * Updates the vertical scroll position and adjusts the margin.
     * @private
     */
    updateVerticalScroll: function() {
        if(this.cfg.scrollable && this.cfg.scrollHeight) {
            if(this.bodyTable.outerHeight() < this.scrollBody.outerHeight()) {
                this.scrollHeaderBox.css('margin-right', '0px');
                this.scrollFooterBox.css('margin-right', '0px');
            }
            else {
                this.scrollHeaderBox.css('margin-right', this.marginRight);
                this.scrollFooterBox.css('margin-right', this.marginRight);
            }
        }
    },

    /**
     * Checks whether the tree table should be sorted.
     * @private
     * @param {JQuery.TriggeredEvent} event Event that occurred.
     * @param {JQuery} column Column that was clicked.
     * @return {boolean} Whether the tree table should be sorted.
     */
    shouldSort: function(event, column) {
        if(this.isEmpty()) {
            return false;
        }

        var target = $(event.target);
        if(target.closest('.ui-column-customfilter', column).length) {
            return false;
        }

        return target.is('th,span');
    },

    /**
     * Checks whether any data is currently displayed.
     * @return {boolean} Whether there is any data displayed currently.
     */
    isEmpty: function() {
        return this.tbody.children('tr.ui-treetable-empty-message').length === 1;
    },

    /**
     * Adds the given sorting to the list of sortings. Each sorting describes a column by which to sort. This data table
     * may be sorted by multiple columns.
     * @param {PrimeFaces.widget.DataTable.SortMeta} meta Sorting to add.
     * @private
     */
    addSortMeta: function(meta) {
        this.sortMeta = $.grep(this.sortMeta, function(value) {
            return value.col !== meta.col;
        });

        this.sortMeta.push(meta);
    },

    /**
     * Serializes the option from the sort meta items.
     * @private
     * @param {keyof PrimeFaces.widget.DataTable.SortMeta} option Property of the sort meta to use.
     * @return {string} All values from the current sort meta list for the given option.
     */
    joinSortMetaOption: function(option) {
        var value = '';

        for(var i = 0; i < this.sortMeta.length; i++) {
            value += this.sortMeta[i][option];

            if(i !== (this.sortMeta.length - 1)) {
                value += ',';
            }
        }

        return value;
    },

    /**
     * Creates the sort order message shown to indicate what the current sort order is.
     * @private
     * @param {string | undefined} ariaLabel Optional label text from an aria attribute.
     * @param {string} sortOrderMessage Sort order message.
     * @return {string} The sort order message to use.
     */
    getSortMessage: function(ariaLabel, sortOrderMessage) {
        var headerName = ariaLabel ? ariaLabel.split(':')[0] : '';
        return headerName + ': ' + sortOrderMessage;
    },

    /**
     * In multi-sort mode this will add number indicators to let the user know the current 
     * sort order. If only one column is sorted then no indicator is displayed and will
     * only be displayed once more than one column is sorted.
     * @private
     */
    updateSortPriorityIndicators: function() {
        var $this = this;

        // remove all indicator numbers first
        $this.sortableColumns.find('.ui-sortable-column-badge').text('').addClass('ui-helper-hidden');

        // add 1,2,3 etc to columns if more than 1 column is sorted
        var sortMeta =  $this.sortMeta;
        if (sortMeta && sortMeta.length > 1) {
            $this.sortableColumns.each(function() {
                var id = $(this).attr("id");
                for (var i = 0; i < sortMeta.length; i++) {
                    if (sortMeta[i].col == id) {
                        $(this).find('.ui-sortable-column-badge').text(i + 1).removeClass('ui-helper-hidden');
                    }
                }
            });
        }
    }
});
/**
 * __PrimeFaces Wizard Widget__
 * 
 * Wizard provides an AJAX enhanced UI to implement a workflow easily in a single page. Wizard consists of several child
 * tab components where each tab represents a step in the process.
 * 
 * @typedef PrimeFaces.widget.Wizard.OnBackCallback Callback that is invoked before switching to the previous wizard
 * step, see also {@link WizardCfg.onback}
 * @this {PrimeFaces.widget.Wizard} PrimeFaces.widget.Wizard.OnBackCallback
 * @return {boolean} PrimeFaces.widget.Wizard.OnBackCallback `true` to switch to the next wizard step, `false` to stay
 * at the current step.
 * 
 * @typedef PrimeFaces.widget.Wizard.OnNextCallback Callback that is invoked before switching to the  next wizard step.
 * If this return `false`, stays on the current tab. See also {@link WizardCfg.onnext}.
 * @this {PrimeFaces.widget.Wizard} PrimeFaces.widget.Wizard.OnNextCallback
 * 
 * @prop {JQuery} backNav The DOM element for the button that switches back to the previous wizard step.
 * @prop {JQuery} content The DOM element for the content of the wizard step.
 * @prop {string} currentStep ID of the currently active wizard step tab.
 * @prop {JQuery} nextNav The DOM element for the button that switches back to the next wizard step.
 * @prop {JQuery} stepControls The DOM element for the container with the wizard step controls.
 * 
 * @interface {PrimeFaces.widget.WizardCfg} cfg The configuration for the {@link  Wizard| Wizard 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 {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {string} cfg.initialStep ID of the wizard step tab that is shown initially.
 * @prop {PrimeFaces.widget.Wizard.OnBackCallback} cfg.onback Callback that is invoked before switching to the previous
 * wizard step. If this returns `false`, stays on the current tab.
 * @prop {PrimeFaces.widget.Wizard.OnNextCallback} cfg.onnext Callback that is invoked before switching to the next
 * wizard step. If this return `false`, stays on the current tab.
 * @prop {boolean} cfg.showStepStatus Whether to display a progress indicator.
 * @prop {boolean} cfg.showNavBar Whether to display a navigation bar.
 * @prop {string[]} cfg.steps List of IDs of the individual wizard step tabs.
 * @prop {string} cfg.effect Animation effect to use when showing and hiding wizard.
 * @prop {number} cfg.effectDuration Duration of the animation effect in milliseconds.
 */
PrimeFaces.widget.Wizard = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.content = $(this.jqId + '_content');
        this.backNav = $(this.jqId + '_back');
        this.nextNav = $(this.jqId + '_next');
        this.cfg.formId = this.jq.parents('form:first').attr('id');
        this.currentStep = this.cfg.initialStep;

        var _self = this;

        //Step controls
        if(this.cfg.showStepStatus) {
            this.stepControls = $(this.jqId + ' .ui-wizard-step-titles li.ui-wizard-step-title');
        }

        //Navigation controls
        if(this.cfg.showNavBar) {
            var currentStepIndex = this.getStepIndex(this.currentStep);

            //visuals
            PrimeFaces.skinButton(this.backNav);
            PrimeFaces.skinButton(this.nextNav);

            //events
            this.backNav.on("click", function() {_self.back();});
            this.nextNav.on("click", function() {_self.next();});

            if(currentStepIndex == 0)
                this.backNav.hide();
            else if(currentStepIndex == this.cfg.steps.length - 1)
                this.nextNav.hide();
        }
    },

    /**
     * Returns to the previous wizard step.
     */
    back: function() {
        var $this = this;
        if(this.cfg.onback) {
            var value = this.cfg.onback.call(this);
            if(value === false) {
                return;
            }
        }

        var targetStepIndex = this.getStepIndex(this.currentStep) - 1;
        if(targetStepIndex >= 0) {
            var stepToGo = this.cfg.steps[targetStepIndex];
            if (this.cfg.effect) {
                this.content.hide($this.cfg.effect, {}, $this.cfg.effectDuration, function() {
                    $this.loadStep(stepToGo, "back");
                    $this.content.show($this.cfg.effect, {}, $this.cfg.effectDuration);
                });
            } else {
                this.loadStep(stepToGo, "back");
            }
        }
    },

    /**
     * Advances to the next wizard step.
     */
    next: function() {
        var $this = this;
        if(this.cfg.onnext) {
            var value = this.cfg.onnext.call(this);
            if(value === false) {
                return;
            }
        }

        var targetStepIndex = this.getStepIndex(this.currentStep) + 1;
        if(targetStepIndex < this.cfg.steps.length) {
            var stepToGo = this.cfg.steps[targetStepIndex];
            if (this.cfg.effect) {
                this.content.hide($this.cfg.effect, {}, $this.cfg.effectDuration, function() {
                    $this.loadStep(stepToGo, "next");
                    $this.content.show($this.cfg.effect, {}, $this.cfg.effectDuration);
                });
            } else {
                this.loadStep(stepToGo, "next");
            }
        }
    },

    /**
     * Loads the given wizard step via AJAX, if not already loaded.
     * @private
     * @param {string} stepToGo ID of the wizard step tab to load. 
     * @param {string} event Type of event that triggered the loading, `back` or `next`. 
     */
    loadStep: function(stepToGo, event) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            formId: this.cfg.formId,
            params: [
                {name: this.id + '_direction', value: event},
                {name: this.id + '_stepToGo', value: stepToGo}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function(xhr, status, args, data) {
                $this.currentStep = args.currentStep;

                if(!args.validationFailed) {
                    var currentStepIndex = $this.getStepIndex($this.currentStep);

                    if($this.cfg.showNavBar) {
                        if(currentStepIndex === $this.cfg.steps.length - 1) {
                            $this.hideNextNav();
                            $this.showBackNav();
                        } else if(currentStepIndex === 0) {
                            $this.hideBackNav();
                            $this.showNextNav();
                        } else {
                            $this.showBackNav();
                            $this.showNextNav();
                        }
                    }

                    //update step status
                    if($this.cfg.showStepStatus) {
                        $this.stepControls.removeClass('ui-state-highlight');
                        $($this.stepControls.get(currentStepIndex)).addClass('ui-state-highlight');
                    }
                }
            }
        };

        if(this.hasBehavior(event)) {
            this.callBehavior(event, options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Finds the index of the given wizard step.
     * @param {string} step ID of the wizard step tab to check.
     * @return {number} The 0-based index of the given wizard step tab.
     */
    getStepIndex: function(step) {
        for(var i=0; i < this.cfg.steps.length; i++) {
            if(this.cfg.steps[i] == step)
                return i;
        }

        return -1;
    },

    /**
     * Shows the button for navigating to the next wizard step.
     */
    showNextNav: function() {
        this.nextNav.fadeIn();
    },

    /**
     * Hides the button for navigating to the next wizard step.
     */
    hideNextNav: function() {
        this.nextNav.fadeOut();
    },

    /**
     * Shows the button for navigating to the previous wizard step.
     */
    showBackNav: function() {
        this.backNav.fadeIn();
    },

    /**
     * Hides the button for navigating to the previous wizard step.
     */
    hideBackNav: function() {
        this.backNav.fadeOut();
    }

});

/**
 * __PrimeFaces TriStateCheckbox Widget__
 * 
 * TriStateCheckbox adds a new state to a checkbox value.
 * 
 * @typedef PrimeFaces.widget.TriStateCheckbox.FixedMod A modulo operation with the result being in the range `[0,mod)`.
 * @param {number} PrimeFaces.widget.TriStateCheckbox.FixedMod.number The divisor for the modulo operation.
 * @param {number} PrimeFaces.widget.TriStateCheckbox.FixedMod.mod The dividend for the modulo operation.
 * @return {number} PrimeFaces.widget.TriStateCheckbox.FixedMod The result of the module operation, in the range
 * `[0,mod)]`.
 * 
 * @prop {JQuery} box The DOM element for the container with the label and the icon.
 * @prop {boolean} disabled Whether this widget is initially disabled.
 * @prop {PrimeFaces.widget.TriStateCheckbox.FixedMod} fixedMod A modulo operation with the result being in the range
 * `[0,mod)`.
 * @prop {JQuery} icon The DOM element for the icon showing the current state of this checkbox widget.
 * @prop {JQuery} input The DOM element for the hidden input field storing the value of this widget.
 * @prop {JQuery} itemLabel The DOM element for the label of the checkbox.
 * 
 * @interface {PrimeFaces.widget.TriStateCheckboxCfg} cfg The configuration for the {@link  TriStateCheckbox| TriStateCheckbox 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
 */
PrimeFaces.widget.TriStateCheckbox = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init:function (cfg) {
        this._super(cfg);

        this.input = $(this.jqId + '_input');
        this.box = this.jq.find('.ui-chkbox-box');
        this.icon = this.box.children('.ui-chkbox-icon');
        this.itemLabel = this.jq.find('.ui-chkbox-label');
        this.disabled = this.input.is(':disabled');
        this.fixedMod = function(number, mod){
            return ((number % mod) + mod) % mod;
        };

        var $this = this;

        //bind events if not disabled
        if (!this.disabled) {
            this.box.on('mouseenter.triStateCheckbox', function () {
                $this.box.addClass('ui-state-hover');
            })
            .on('mouseleave.triStateCheckbox', function () {
                $this.box.removeClass('ui-state-hover');
            })
            .on('click.triStateCheckbox', function () {
                $this.toggle(1);
                $this.input.trigger('focus');
            });
            
            this.input.on('focus.triStateCheckbox', function () {
                $this.box.addClass('ui-state-focus');
            })
            .on('blur.triStateCheckbox', function () {
                $this.box.removeClass('ui-state-focus');
            })
            .on('keydown.triStateCheckbox', function (e) {
                var keyCode = $.ui.keyCode;

                switch (e.which) {
                    case keyCode.SPACE:
                    case keyCode.UP:
                    case keyCode.RIGHT:
                    case keyCode.LEFT:
                    case keyCode.DOWN:
                        e.preventDefault();
                        break;
                }
            })            
            .on('keyup.triStateCheckbox', function (e) {
                var keyCode = $.ui.keyCode;

                switch(e.which) {
                    case keyCode.SPACE:
                    case keyCode.UP:
                    case keyCode.RIGHT:
                        $this.toggle(1);
                        break;
                    case keyCode.LEFT:
                    case keyCode.DOWN:
                        $this.toggle(-1);
                        break;
                }
            });

            //toggle state on label click
            this.itemLabel.on('click.triStateCheckbox', function() {
                $this.toggle(1);
                $this.input.trigger('focus');
            });

            // client behaviors
            if (this.cfg.behaviors) {
                PrimeFaces.attachBehaviors(this.input, this.cfg.behaviors);
            }
        }

        // pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
    },

    /**
     * Toggles this tri state checkbox in the given direction. Moves between unchecked, half-checked, and fully-checked.
     * @param {-1 | 1} direction `-1` to move backwards through the states, `+1` to move forward through the states 
     */
    toggle:function (direction) {
        if (!this.disabled) {
            // default to switch to next state
            if (isNaN(direction)) {
                direction = 1;
            }

            var oldValue = parseInt(this.input.val());
            var newValue = this.fixedMod((oldValue + direction), 3);
            this.input.val(newValue);

            // remove / add def. icon and active classes
            if (newValue == 0) {
                this.box.removeClass('ui-state-active');
            } else {
                this.box.addClass('ui-state-active');
            }

            // remove old icon and add the new one
            var iconsClasses = this.box.data('iconstates');
            this.icon.removeClass(iconsClasses[oldValue]).addClass(iconsClasses[newValue]);

            // change title to the new one
            var iconTitles = this.box.data('titlestates');
            if (iconTitles != null && iconTitles.titles != null && iconTitles.titles.length > 0) {
                this.box.attr('title', iconTitles.titles[newValue]);
            }

            // fire change event
            this.input.trigger('change');
        }
    }
});


/**
 * __PrimeFaces Chip Widget__
 *
 * Chip represents entities using icons, labels and images.
 *
 * @prop {JQuery} removeIcon DOM element of the icon for closing this chip, when this chip is closable (an `x` by
 * default).
 *
 * @interface {PrimeFaces.widget.ChipCfg} cfg The configuration for the {@link  Chip| Chip 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
 */
PrimeFaces.widget.Chip = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.removeIcon = this.jq.children('.ui-chip-remove-icon');

        this.bindEvents();
    },

    /**
     * Sets up all event listeners required for this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;
        
        this.jq.on("click.chip", function() {
           $this.callBehavior("select");
        });

        this.removeIcon.on("keydown.chip", function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                $this.close();
                e.preventDefault();
            }
        }).on("click.chip", function() {
            $this.close();
        });
    },

    /**
     * Closes the chip.
     * @private
     */
    close: function() {
        this.jq.remove();
        this.callBehavior("close");
    }
});

/**
 * __PrimeFaces Chips Widget__
 * 
 * Chips is used to enter multiple values on an inputfield.
 * 
 * @prop {JQuery} input DOM element of the visible INPUT field.
 * @prop {JQuery} hinput DOM element of the hidden INPUT field with the current value.
 * @prop {JQuery} itemContainer DOM element of the container of the items (chips).
 * @prop {JQuery} inputContainer DOM element of the container for the visible INPUT.
 * @prop {string} placeholder Placeholder for the input field.
 * 
 * @interface {PrimeFaces.widget.ChipsCfg} cfg The configuration for the {@link  Chips| Chips 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.addOnBlur Whether to add an item when the input loses focus.
 * @prop {boolean} cfg.unique Prevent duplicate entries from being added.
 * @prop {number} cfg.max Maximum number of entries allowed.
 * @prop {string} cfg.separator Separator character to allow multiple values such if a list is pasted into the input.
 * Default is `,`.
 */
PrimeFaces.widget.Chips = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);
        this.cfg.separator = this.cfg.separator||',';

        this.input = $(this.jqId + '_input');
        this.hinput = $(this.jqId + '_hinput');
        this.itemContainer = this.jq.children('ul');
        this.inputContainer = this.itemContainer.children('.ui-chips-input-token');

        //pfs metadata
        this.input.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        this.hinput.data(PrimeFaces.CLIENT_ID_DATA, this.id);
        this.placeholder = this.input.attr('placeholder');

        this.bindEvents();
    },

    /**
     * Sets up all event listeners required for this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        this.itemContainer.on("mouseenter", function() {
            $(this).addClass('ui-state-hover');
        }).on("mouseleave", function() {
            $(this).removeClass('ui-state-hover');
        }).on("click", function() {
            $this.input.trigger('focus');
        });


        this.input.on('focus.chips', function() {
            $this.itemContainer.addClass('ui-state-focus');
        }).on('blur.chips', function() {
            $this.itemContainer.removeClass('ui-state-focus');

            if ($this.cfg.addOnBlur) {
                $this.addItem($(this).val(), false);
            }
        }).on('paste.chips', function(e) {
            if ($this.cfg.addOnPaste) {
                var pasteData = e.originalEvent.clipboardData.getData('text');
                $this.addItem(pasteData, false);
                e.preventDefault();
                e.stopPropagation();
            }
        }).on('keydown.chips', function(e) {
            var keyCode = $.ui.keyCode;
            var value = $(this).val();

            switch(e.which) {
                case keyCode.BACKSPACE:
                    if(value.length === 0 && $this.hinput.children('option') && $this.hinput.children('option').length > 0) {
                        var lastOption = $this.hinput.children('option:last'),
                            index = lastOption.index();
                        $this.removeItem($($this.itemContainer.children('li.ui-chips-token').get(index)));
                    }
                break;

                case keyCode.ENTER:
                    $this.addItem(value, true);
                    e.preventDefault();
                    e.stopPropagation();
                break;

                default:
                    if($this.cfg.max && $this.cfg.max === $this.hinput.children('option').length) {
                        e.preventDefault();
                    }
                break;
            }
        });

        var closeSelector = '> li.ui-chips-token > .ui-chips-token-icon';
        this.itemContainer.off('click', closeSelector).on('click', closeSelector, null, function(event) {
            $this.removeItem($(this).parent());
        });
    },

    /**
     * Adds a new item (chip) to the list of currently displayed items.
     * @param {string} value Value of the chip to add.
     * @param {boolean} [refocus] `true` to put focus back on the INPUT again after the chip was added, or `false`
     * otherwise. 
     */
    addItem: function(value, refocus) {
        var $this = this;

        if (!value || !value.trim().length) {
            return;
        }

        var tokens = value.split(this.cfg.separator);
        for (var i = 0; i < tokens.length; i++) {
           var token = tokens[i];
           if(token && token.trim().length && (!this.cfg.max||this.cfg.max > this.hinput.children('option').length)) {
               var escapedValue = PrimeFaces.escapeHTML(token);

               if (this.cfg.unique) {
                   var duplicateFound = false;
                   this.hinput.children('option').each(function() {
                       if (this.value === escapedValue) {
                           $this.refocus(refocus);
                           duplicateFound = true;
                           return false; // breaks
                       }
                   });
                   if (duplicateFound) {
                       return;
                   }
               }

               var itemDisplayMarkup = '<li class="ui-chips-token ui-state-active ui-corner-all">';
               itemDisplayMarkup += '<span class="ui-chips-token-icon ui-icon ui-icon-close"></span>';
               itemDisplayMarkup += '<span class="ui-chips-token-label">' + escapedValue + '</span></li>';

               this.inputContainer.before(itemDisplayMarkup);
               this.refocus(refocus);

               this.hinput.append('<option value="' + escapedValue + '" selected="selected"></option>');
               this.invokeItemSelectBehavior(escapedValue);
            }
        }
    },

    /**
     * Deletes the currently editing input value and refocus the input box if necessary.
     * @param {boolean} [refocus] `true` to put focus back on the INPUT again after the chip was added, or `false`
     * otherwise. 
     * @private
     */
    refocus: function(refocus) {
        this.input.val('');
        this.input.removeAttr('placeholder');

        if (refocus) {
            this.input.trigger('focus');
        }
    },

    /**
     * Removes an item (chip) from the list of currently displayed items.
     * @param {JQuery} item An item (LI element) that should be removed.
     * @param {boolean} [silent] Flag indicating whether to animate the removal and fire the AJAX behavior.
     */
    removeItem: function(item, silent) {
        var itemIndex = this.itemContainer.children('li.ui-chips-token').index(item);
        var itemValue = item.find('span.ui-chips-token-label').html()
        $this = this;

        //remove from options
        this.hinput.children('option').eq(itemIndex).remove();

        if(silent) {
            item.remove();
        }
        else {
            item.fadeOut('fast', function() {
                var token = $(this);
                token.remove();
                $this.invokeItemUnselectBehavior(itemValue);
            });
        }

        // if empty return placeholder
        if (this.placeholder && this.hinput.children('option').length === 0) {
            this.input.attr('placeholder', this.placeholder);
        }
    },

    /**
     * Converts the current list into a separator delimited list for mass editing while keeping original
     * order of the items or closes the editor turning the values back into chips.
     */
    toggleEditor: function() {
        var $this = this,
            tokens = this.itemContainer.children('li.ui-chips-token');
  
        if(tokens.length) {
            var editor = '';
            tokens.each(function() {
                var token = $(this),
                    tokenValue = token.find('span.ui-chips-token-label').html();
                editor = editor + tokenValue +  $this.cfg.separator;
                $this.removeItem(token, true);
            });

            if(editor) {
                editor = editor.slice(0, -1); 
                this.input.val(editor);
            }
        }
        else {
            $this.addItem(this.input.val(), true);
        }
    },

    /**
     * Triggers the behaviors and event listeners for when an item (chip) was selected.
     * @param {string} itemValue Value of the selected item.
     * @private
     */
    invokeItemSelectBehavior: function(itemValue) {
        if(this.hasBehavior('itemSelect')) {
            var ext = {
                params : [
                    {name: this.id + '_itemSelect', value: itemValue}
                ]
            };

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

    /**
     * Triggers the behaviors and event listeners for when an item (chip) was unselected.
     * @param {string} itemValue Value of the unselected item.
     * @private 
     */
    invokeItemUnselectBehavior: function(itemValue) {
        if(this.hasBehavior('itemUnselect')) {
            var ext = {
                params : [
                    {name: this.id + '_itemUnselect', value: itemValue}
                ]
            };

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

/**
 * __PrimeFaces ScrollTop Widget__
 *
 * ScrollTop gets displayed after a certain scroll position and used to navigates to the top of the page quickly.
 *
 * @prop {JQuery} scrollElement Window or parent element of the ScrollTop.
 *
 * @interface {PrimeFaces.widget.ScrollTopCfg} cfg The configuration for the {@link  ScrollTop| ScrollTop 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 {string} cfg.behavior Scrolling behavior of the ScrollTop.
 * @prop {string} cfg.target Target element of the scroll top widget.
 * @prop {number} cfg.threshold Value of the vertical scroll position of the target to toggle the visibility.
 */
PrimeFaces.widget.ScrollTop = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.scrollElement = this.cfg.target === 'window' ? $(window) : this.jq.parent();
        
        this.bindEvents();
    },

    /**
     * Sets up all event listeners required for this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this,
        scrollNS = 'scroll.scrollTop' + this.id,
        zIndex = $this.jq.css('zIndex');
        
        this.jq.on('click.scrollTop', function(e) {
            $this.scrollElement.get(0).scroll({
                top: 0,
                behavior: $this.cfg.behavior
            });
            
            e.preventDefault();
        });

        this.scrollElement.off(scrollNS).on(scrollNS, function() {
            if ($this.cfg.threshold < $this.scrollElement.scrollTop()) {
                $this.jq.fadeIn({
                    duration: 150,
                    start: function() {
                        if (zIndex === 'auto' && $this.jq.css('zIndex') === 'auto') {
                            $this.jq.css('zIndex', PrimeFaces.nextZindex());
                        }
                    }
                });
            }
            else {
                $this.jq.fadeOut({
                    duration: 150,
                    start: function() {
                        if (zIndex === 'auto') {
                            $this.jq.css('zIndex', '');
                        }
                    }
                });
            }
        });
    }
});
/**
 * __PrimeFaces Sidebar Widget__
 * 
 * Sidebar is a panel component displayed as an overlay at the edges of the screen.
 * 
 * @prop {JQuery} closeIcon The DOM element for the icon that closes this sidebar.
 * @prop {boolean} loaded When dynamic loading is enabled, whether the content was already loaded.
 * 
 * @typedef PrimeFaces.widget.Sidebar.OnHideCallback Callback that is invoked when the sidebar is opened. See also
 * {@link SidebarCfg.onHide}.
 * @this {PrimeFaces.widget.Sidebar} PrimeFaces.widget.Sidebar.OnHideCallback 
 * 
 * @typedef PrimeFaces.widget.Sidebar.OnShowCallback Callback that is invoked when the sidebar is closed. See also
 * {@link SidebarCfg.onShow}.
 * @this {PrimeFaces.widget.Sidebar} PrimeFaces.widget.Sidebar.OnShowCallback 
 * 
 * @interface {PrimeFaces.widget.SidebarCfg} cfg The configuration for the {@link  Sidebar| Sidebar 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.DynamicOverlayWidgetCfg} cfg
 * 
 * @prop {boolean} cfg.modal Whether the sidebar is modal and blocks the main content and other dialogs.
 * @prop {boolean} cfg.showCloseIcon Whether the close icon is displayed.
 * @prop {string} cfg.appendTo The search expression for the element to which the overlay panel should be appended.
 * @prop {number} cfg.baseZIndex Base z-index for the sidebar.
 * @prop {PrimeFaces.widget.Sidebar.OnHideCallback} cfg.onHide Callback that is invoked when the sidebar is opened.
 * @prop {PrimeFaces.widget.Sidebar.OnShowCallback} cfg.onShow Callback that is invoked when the sidebar is closed.
 * @prop {boolean} cfg.visible Whether the sidebar is initially opened.
 * @prop {boolean} cfg.dynamic `true` to load the content via AJAX when the overlay panel is opened, `false` to load
 * the content immediately.
 */
PrimeFaces.widget.Sidebar = PrimeFaces.widget.DynamicOverlayWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.cfg.modal = (this.cfg.modal === true || this.cfg.modal === undefined);
        this.cfg.showCloseIcon = (this.cfg.showCloseIcon === true || this.cfg.showCloseIcon === undefined);
        this.cfg.baseZIndex = this.cfg.baseZIndex||0;

        if(this.cfg.showCloseIcon) {
            this.closeIcon = this.jq.children('.ui-sidebar-close');
        }

        //aria
        this.applyARIA();

        if(this.cfg.visible){
            this.show();
        }

        this.bindEvents();
    },

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    refresh: function(cfg) {
        this._super(cfg);

        this.loaded = false;
    },

    /**
     * Sets up all event listeners that are required by this widget.
     * @private
     */
    bindEvents: function() {
        var $this = this;

        if(this.cfg.showCloseIcon) {
            this.closeIcon.off('mouseover mouseout focus blur click').on('mouseover', function() {
                $(this).addClass('ui-state-hover');
            }).on('mouseout', function() {
                $(this).removeClass('ui-state-hover');
            }).on('focus', function() {
                $(this).addClass('ui-state-focus');
            }).on('blur', function() {
                $(this).removeClass('ui-state-focus');
            }).on('click', function(e) {
                $this.hide();
                e.preventDefault();
            });
        }
    },

    /**
     * Brings up this sidebar in case is is not already visible.
     */
    show: function() {
        if(this.isVisible()) {
            return;
        }

        if (!this.loaded && this.cfg.dynamic) {
            this.loadContents();
        }
        else {
            this._show();
        }
    },

    /**
     * Makes the sidebar panel visible.
     * @private
     */
    _show: function() {
        this.jq.addClass('ui-sidebar-active');
        this.jq.css('z-index', String(this.cfg.baseZIndex + (++PrimeFaces.zindex)));

        this.postShow();

        if(this.cfg.modal) {
            this.enableModality();
        }
    },

    /**
     * Callback function that is invoked when this sidebar is hidden.
     * @private
     */
    postShow: function() {
        PrimeFaces.invokeDeferredRenders(this.id);

        //execute user defined callback
        if(this.cfg.onShow) {
            this.cfg.onShow.call(this);
        }
    },

    /**
     * Hides this sidebara in case it is not already hidden.
     */
    hide: function() {
        if(!this.isVisible()) {
            return;
        }

        this.jq.removeClass('ui-sidebar-active');
        this.onHide();

        if(this.cfg.modal) {
            this.disableModality();
        }
    },

    /**
     * Checks whether this sidebar is currently visible.
     * @return {boolean} `true` if this sideplay is visible, or `false` otherwise.
     */
    isVisible: function() {
        return this.jq.hasClass('ui-sidebar-active');
    },

    /**
     * Callback function that is invoked when this sidebar is hidden.
     * @private
     * @param {JQuery.TriggeredEvent} event Currently unused.
     * @param {unknown} ui Currently unused.
     */
    onHide: function(event, ui) {
        if(this.cfg.onHide) {
            this.cfg.onHide.call(this, event, ui);
        }
    },

    /**
     * Hides this sidebar if it is visible or brings it up if it is hidden.
     */
    toggle: function() {
        if(this.isVisible())
            this.hide();
        else
            this.show();
    },

    /**
     * @override
     * @inheritdoc
     */
    enableModality: function() {
        this._super();

        var $this = this;
        this.modalOverlay.one('click.sidebar', function() {
            $this.hide();
        });
    },

    /**
     * @override
     * @inheritdoc
     * @return {JQuery}
     */
    getModalTabbables: function(){
        return this.jq.find(':tabbable');
    },

    /**
     * Sets all ARIA attributes on the elements and the icons.
     * @private
     */
    applyARIA: function() {
        this.jq.attr({
            'role': 'dialog'
            ,'aria-hidden': !this.cfg.visible
            ,'aria-modal': this.cfg.modal && this.cfg.visible
        });

        if(this.cfg.showCloseIcon) {
            this.closeIcon.attr('role', 'button');
        }
    },

    /**
     * Loads the contents of this sidebar panel dynamically via AJAX, if dynamic loading is enabled.
     * @private
     */
    loadContents: function() {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_contentLoad', value: true}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            $this.jq.html(content);
                            $this.loaded = true;
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this._show();
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    }

});
/**
 * __PrimeFaces DataView Widget__
 * 
 * DataView displays data in grid or list layout.
 * 
 * @typedef {"grid" | "list"} PrimeFaces.widget.DataView.Layout The layout mode the data view. `grid` displays the
 * item in a grid with cards, `list` displays the items in a vertical list.
 * 
 * @prop {JQuery} buttons DOM elements of the buttons for switching the layout (grid or list).
 * @prop {JQuery} content DOM element of the content container for the data grid.
 * @prop {JQuery} header DOM element of the data view header. 
 * @prop {JQuery} layoutOptions DOM element of the container with the layout switch buttons.
 * @prop {PrimeFaces.widget.Paginator} paginator When pagination is enabled: The paginator widget instance used for
 * paging.
 * 
 * @interface {PrimeFaces.widget.DataViewCfg} cfg The configuration for the {@link  DataView| DataView 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 {string} cfg.formId ID of the form to use for AJAX requests.
 * @prop {Partial<PrimeFaces.widget.PaginatorCfg>} cfg.paginator When pagination is enabled: The paginator configuration
 * for the paginator.
 */
PrimeFaces.widget.DataView = PrimeFaces.widget.BaseWidget.extend({

    /**
     * @override
     * @inheritdoc
     * @param {PrimeFaces.PartialWidgetCfg<TCfg>} cfg
     */
    init: function(cfg) {
        this._super(cfg);

        this.header = this.jq.children('.ui-dataview-header');
        this.content = this.jq.children('.ui-dataview-content');
        this.layoutOptions = this.header.children('.ui-dataview-layout-options');
        this.buttons = this.layoutOptions.children('div');

        if(this.cfg.paginator) {
            this.setupPaginator();
        }

        this.bindEvents();
    },

    /**
     * Initializes the paginator, called during widget initialization.
     * @private
     */
    setupPaginator: function() {
        var $this = this;
        this.cfg.paginator.paginate = function(newState) {
            $this.handlePagination(newState);
        };

        this.paginator = new PrimeFaces.widget.Paginator(this.cfg.paginator);
        this.paginator.bindSwipeEvents(this.jq, this.cfg);
    },

    /**
     * Sets up all event listeners required by this widget.
     * @private
     */
    bindEvents: function () {
        var $this = this;

        this.buttons.on('mouseover', function() {
            var button = $(this);
            button.addClass('ui-state-hover');
        })
        .on('mouseout', function() {
            $(this).removeClass('ui-state-hover');
        })
        .on('click', function() {
            var button = $(this),
            radio = button.children(':radio');

            if(!radio.prop('checked')) {
                $this.select(button);
            }
        });

        /* For keyboard accessibility */
        this.buttons.on('focus.dataview-button', function(){
            var button = $(this);
            button.addClass('ui-state-focus');
        })
        .on('blur.dataview-button', function(){
            var button = $(this);
            button.removeClass('ui-state-focus');
        })
        .on('keydown.dataview-button', function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER) {
                var button = $(this),
                radio = button.children(':radio');

                if(!radio.prop('checked')) {
                    $this.select(button);
                }
                e.preventDefault();
            }
        });
    },

    /**
     * Switches this data view to the given layout (grid or list).
     * 
     * ```javascript
     * const widget = PF("MyDataView");
     * // Switch to grid layout
     * widget.select(widget.buttons.eq(1));
     * ```
     * @param {JQuery} button One of the layout switch buttons (`.ui-button`).
     */
    select: function(button) {
        this.buttons.filter('.ui-state-active').removeClass('ui-state-active ui-state-hover').children(':radio').prop('checked', false);

        button.addClass('ui-state-active').children(':radio').prop('checked', true);

        this.loadLayoutContent(button.children(':radio').val());
    },

    /**
     * Loads the content with the data items for the selected layout (grid or list).
     * @private
     * @param {PrimeFaces.widget.DataView.Layout} layout The current layout of this data view.
     */
    loadLayoutContent: function(layout) {
        var $this = this,
        options = {
            source: this.id,
            process: this.id,
            update: this.id,
            params: [
                {name: this.id + '_layout', value: layout}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.jq.removeClass('ui-dataview-grid ui-dataview-list').addClass('ui-dataview-' + layout);
            }
        };

        PrimeFaces.ajax.Request.handle(options);
    },

    /**
     * Handles a pagination event by updating the data grid and invoking the appropriate behaviors.
     * @private
     * @param {PrimeFaces.widget.Paginator.PaginationState} newState The new pagination state to apply. 
     */
    handlePagination: function(newState) {
        var $this = this,
        options = {
            source: this.id,
            update: this.id,
            process: this.id,
            formId: this.getParentFormId(),
            params: [
                {name: this.id + '_pagination', value: true},
                {name: this.id + '_first', value: newState.first},
                {name: this.id + '_rows', value: newState.rows}
            ],
            onsuccess: function(responseXML, status, xhr) {
                PrimeFaces.ajax.Response.handle(responseXML, status, xhr, {
                        widget: $this,
                        handle: function(content) {
                            this.content.html(content);
                        }
                    });

                return true;
            },
            oncomplete: function() {
                $this.paginator.cfg.page = newState.page;
                $this.paginator.updateUI();
            }
        };

        if(this.hasBehavior('page')) {
            this.callBehavior('page', options);
        }
        else {
            PrimeFaces.ajax.Request.handle(options);
        }
    },

    /**
     * Retrieves the paginator widget used by this data grid for pagination. You can use this widget to switch to a
     * different page programatically.
     * @return {PrimeFaces.widget.Paginator | undefined} The paginator widget, or `undefined` when pagination is not
     * enabled.
     */
    getPaginator: function() {
        return this.paginator;
    }
});
