/*!
 * jQuery Pretty Dropdowns Plugin v4.17.0 by T. H. Doan (https://thdoan.github.io/pretty-dropdowns/)
 *
 * jQuery Pretty Dropdowns by T. H. Doan is licensed under the MIT License.
 * Read a copy of the license in the LICENSE file or at https://choosealicense.com/licenses/mit/
 */

(function($) {
    $.fn.prettyDropdown = function(oOptions) {

        // Default options
        oOptions = $.extend({
            classic: false,
            customClass: 'arrow',
            width: null,
            height: 50,
            hoverIntent: 200,
            multiDelimiter: '; ',
            multiVerbosity: 99,
            selectedMarker: '✓',
            afterLoad: function(){}
        }, oOptions);

        oOptions.selectedMarker = '<span aria-hidden="true" class="checked"> ' + oOptions.selectedMarker + '</span>';
        // Validate options
        if (isNaN(oOptions.width) && !/^\d+%$/.test(oOptions.width)) oOptions.width = null;
        if (isNaN(oOptions.height)) oOptions.height = 50;
        else if (oOptions.height<8) oOptions.height = 8;
        if (isNaN(oOptions.hoverIntent)) oOptions.hoverIntent = 200;
        if (isNaN(oOptions.multiVerbosity)) oOptions.multiVerbosity = 99;

        // Translatable strings
        var MULTI_NONE = 'None selected',
            MULTI_PREFIX = 'Selected: ',
            MULTI_POSTFIX = ' selected';

        // Globals
        var $current,
            aKeys = [
                '0','1','2','3','4','5','6','7','8','9',,,,,,,,
                'a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'
            ],
            nCount,
            nHoverIndex,
            nLastIndex,
            nTimer,
            nTimestamp,

            // Initiate pretty drop-downs
            init = function(elSel) {
                var $select = $(elSel),
                    nSize = elSel.size,
                    sId = elSel.name || elSel.id || '',
                    sLabelId;
                // Exit if widget has already been initiated
                if ($select.data('loaded')) return;
                // Remove 'size' attribute to it doesn't affect vertical alignment
                $select.data('size', nSize).removeAttr('size');
                // Set <select> height to reserve space for <div> container
                $select.css('visibility', 'hidden').outerHeight(oOptions.height);
                nTimestamp = performance.now()*100000000000000;
                // Test whether to add 'aria-labelledby'
                if (elSel.id) {
                    // Look for <label>
                    var $label = $('label[for=' + elSel.id + ']');
                    if ($label.length) {
                        // Add 'id' to <label> if necessary
                        if ($label.attr('id') && !/^menu\d{13,}$/.test($label.attr('id'))) sLabelId = $label.attr('id');
                        else $label.attr('id', (sLabelId = 'menu' + nTimestamp));
                    }
                }
                nCount = 0;
                var $items = $('optgroup, option', $select),
                    $selected = $items.filter(':selected'),
                    bMultiple = elSel.multiple,
                    // Height - 2px for borders
                    sHtml = '<ul' + (elSel.disabled ? '' : ' tabindex="0"') + ' role="listbox"'
                        + (elSel.title ? ' title="' + elSel.title + '" aria-label="' + elSel.title + '"' : '')
                        + (sLabelId ? ' aria-labelledby="' + sLabelId + '"' : '')
                        + ' aria-activedescendant="item' + nTimestamp + '-1" aria-expanded="false"'
                        + ' style="max-height:' + (oOptions.height-2) + 'px;margin:'
                        // NOTE: $select.css('margin') returns an empty string in Firefox, so we have to get
                        // each margin individually. See https://github.com/jquery/jquery/issues/3383
                        + $select.css('margin-top') + ' '
                        + $select.css('margin-right') + ' '
                        + $select.css('margin-bottom') + ' '
                        + $select.css('margin-left') + ';">';
                if (bMultiple) {
                    sHtml += renderItem(null, 'selected');
                    $items.each(function() {
                        if (this.selected) {
                            sHtml += renderItem(this, '', true)
                        } else {
                            sHtml += renderItem(this);
                        }
                    });
                } else {
                    if (oOptions.classic) {
                        $items.each(function() {
                            sHtml += renderItem(this);
                        });
                    } else {
                        sHtml += renderItem($selected[0], 'selected');
                        $items.filter(':not(:selected)').each(function() {
                            sHtml += renderItem(this);
                        });
                    }
                }
                sHtml += '</ul>';
                $select.wrap('<div ' + (sId ? 'id="prettydropdown-' + sId + '" ' : '')
                    + 'class="prettydropdown '
                    + (oOptions.classic ? 'classic ' : '')
                    + (elSel.disabled ? 'disabled ' : '')
                    + (bMultiple ? 'multiple ' : '')
                    + oOptions.customClass + ' loading"'
                    // NOTE: For some reason, the container height is larger by 1px if the <select> has the
                    // 'multiple' attribute or 'size' attribute with a value larger than 1. To fix this, we
                    // have to inline the height.
                    + ((bMultiple || nSize>1) ? ' style="height:' + oOptions.height + 'px;"' : '')
                    +'></div>').before(sHtml).data('loaded', true);
                var $dropdown = $select.parent().children('ul'),
                    nWidth = $dropdown.outerWidth(true),
                    nOuterWidth;
                $items = $dropdown.children();
                // Update default selected values for multi-select menu
                if (bMultiple) updateSelected($dropdown);
                else if (oOptions.classic) $('[data-value="' + $selected.val() + '"]', $dropdown).addClass('selected').append(oOptions.selectedMarker);
                // Calculate width if initially hidden
                if ($dropdown.width()<=0) {
                    var $clone = $dropdown.parent().clone().css({
                        position: 'absolute',
                        top: '-100%'
                    });
                    $('body').append($clone);
                    nWidth = $clone.children('ul').outerWidth(true);
                    $('li', $clone).width(nWidth);
                    nOuterWidth = $clone.children('ul').outerWidth(true);
                    $clone.remove();
                }
                // Set dropdown width and event handler
                // NOTE: Setting width using width(), then css() because width() only can return a float,
                // which can result in a missing right border when there is a scrollbar.
                $items.width(nWidth).css('width', $items.css('width'));
                if (oOptions.width) {
                    $dropdown.parent().css('min-width', $items.css('width'));
                    $dropdown.css('width', '100%');
                    $items.css('width', '100%');
                }
                $items.click(function() {
                    var $li = $(this),
                        $selected = $dropdown.children('.selected');
                    // Ignore disabled menu
                    if ($dropdown.parent().hasClass('disabled')) return;
                    // Only update if not disabled, not a label, and a different value selected
                    if ($dropdown.hasClass('active') && !$li.hasClass('disabled') && !$li.hasClass('label') && $li.data('value')!==$selected.data('value')) {
                        // Select highlighted item
                        if (bMultiple) {
                            if ($li.children('span.checked').length) $li.children('span.checked').remove();
                            else $li.append(oOptions.selectedMarker);
                            // Sync <select> element
                            $dropdown.children(':not(.selected)').each(function(nIndex) {
                                $('optgroup, option', $select).eq(nIndex).prop('selected', $(this).children('span.checked').length>0);
                            });
                            // Update selected values for multi-select menu
                            updateSelected($dropdown);
                        } else {
                            $selected.removeClass('selected').children('span.checked').remove();
                            $li.addClass('selected').append(oOptions.selectedMarker);
                            if (!oOptions.classic) $dropdown.prepend($li);
                            $dropdown.removeClass('reverse').attr('aria-activedescendant', $li.attr('id'));
                            if ($selected.data('group') && !oOptions.classic) $dropdown.children('.label').filter(function() {
                                return $(this).text()===$selected.data('group');
                            }).after($selected);
                            // Sync <select> element
                            $('optgroup, option', $select).filter(function() {
                                // <option>: this.value = this.text, $li.data('value') = $li.contents()
                                // <option value="">: this.value = "", $li.data('value') = undefined
                                return (this.value+'|'+this.text)===($li.data('value')||'')+'|'+$li.contents().filter(function() {
                                    // Filter out selected marker
                                    return this.nodeType===3;
                                }).text();
                            }).prop('selected', true);
                        }
                        $select.trigger('change');
                    }
                    if ($li.hasClass('selected') || !bMultiple) {
                        $dropdown.toggleClass('active');
                        $dropdown.attr('aria-expanded', $dropdown.hasClass('active'));
                    }
                    // Try to keep drop-down menu within viewport
                    if ($dropdown.hasClass('active')) {
                        // Close any other open menus
                        if ($('.prettydropdown > ul.active').length>1) resetDropdown($('.prettydropdown > ul.active').not($dropdown)[0]);
                        var nWinHeight = window.innerHeight,
                            nMaxHeight,
                            nOffsetTop = $dropdown.offset().top,
                            nScrollTop = $(document).scrollTop(),
                            nDropdownHeight = $dropdown.outerHeight();
                        if (nSize) {
                            nMaxHeight = nSize*(oOptions.height-2);
                            if (nMaxHeight<nDropdownHeight-2) nDropdownHeight = nMaxHeight+2;
                        }
                        var nDropdownBottom = nOffsetTop-nScrollTop+nDropdownHeight;
                        if (nDropdownBottom>nWinHeight) {
                            // Expand to direction that has the most space
                            if (nOffsetTop-nScrollTop>nWinHeight-(nOffsetTop-nScrollTop+oOptions.height)) {
                                $dropdown.addClass('reverse');
                                if (!oOptions.classic) $dropdown.append($selected);
                                if (nOffsetTop-nScrollTop+oOptions.height<nDropdownHeight) {
                                    $dropdown.outerHeight(nOffsetTop-nScrollTop+oOptions.height);
                                    // Ensure the selected item is in view
                                    $dropdown.scrollTop(nDropdownHeight);
                                }
                            } else {
                                $dropdown.height($dropdown.height()-(nDropdownBottom-nWinHeight));
                            }
                        }
                        if (nMaxHeight && nMaxHeight<$dropdown.height()) $dropdown.css('height', nMaxHeight + 'px');
                        // Ensure the selected item is in view
                        if (oOptions.classic) $dropdown.scrollTop($selected.index()*(oOptions.height-2));
                    } else {
                        $dropdown.data('clicked', true);
                        resetDropdown($dropdown[0]);
                    }
                });
                $dropdown.on({
                    focusin: function() {
                        // Unregister any existing handlers first to prevent duplicate firings
                        $(window).off('keydown', handleKeypress).on('keydown', handleKeypress);
                    },
                    focusout: function() {
                        $(window).off('keydown', handleKeypress);
                    },
                    mouseenter: function() {
                        $dropdown.data('hover', true);
                    },
                    mouseleave: resetDropdown,
                    mousemove:  hoverDropdownItem
                });
                if (oOptions.hoverIntent<0) {
                    $(document).click(function(e) {
                        if ($dropdown.data('hover') && !$dropdown[0].contains(e.target)) resetDropdown($dropdown[0]);
                    });
                }
                // Put focus on menu when user clicks on label
                if (sLabelId) $('#' + sLabelId).off('click', handleFocus).click(handleFocus);
                // Done with everything!
                $dropdown.parent().width(oOptions.width||nOuterWidth||$dropdown.outerWidth(true)).removeClass('loading');
                oOptions.afterLoad();
            },

            // Manage widget focusing
            handleFocus = function(e) {
                $('ul[aria-labelledby=' + e.target.id + ']').focus();
            },

            // Manage keyboard navigation
            handleKeypress = function(e) {
                var $dropdown = $('.prettydropdown > ul.active, .prettydropdown > ul:focus');
                if (!$dropdown.length) return;
                if (e.which===9) { // Tab
                    resetDropdown($dropdown[0]);
                    return;
                } else {
                    // Intercept non-Tab keys only
                    e.preventDefault();
                    e.stopPropagation();
                }
                var $items = $dropdown.children(),
                    bOpen = $dropdown.hasClass('active'),
                    nItemsHeight = $dropdown.height()/(oOptions.height-2),
                    nItemsPerPage = nItemsHeight%1<0.5 ? Math.floor(nItemsHeight) : Math.ceil(nItemsHeight),
                    sKey;
                nHoverIndex = Math.max(0, $dropdown.children('.hover').index());
                nLastIndex = $items.length-1;
                $current = $items.eq(nHoverIndex);
                $dropdown.data('lastKeypress', +new Date());
                switch (e.which) {
                    case 13: // Enter
                        if (!bOpen) {
                            $current = $items.filter('.selected');
                            toggleHover($current, 1);
                        }
                        $current.click();
                        break;
                    case 27: // Esc
                        if (bOpen) resetDropdown($dropdown[0]);
                        break;
                    case 32: // Space
                        if (bOpen) {
                            sKey = ' ';
                        } else {
                            $current = $items.filter('.selected');
                            toggleHover($current, 1);
                            $current.click();
                        }
                        break;
                    case 33: // Page Up
                        if (bOpen) {
                            toggleHover($current, 0);
                            toggleHover($items.eq(Math.max(nHoverIndex-nItemsPerPage-1, 0)), 1);
                        }
                        break;
                    case 34: // Page Down
                        if (bOpen) {
                            toggleHover($current, 0);
                            toggleHover($items.eq(Math.min(nHoverIndex+nItemsPerPage-1, nLastIndex)), 1);
                        }
                        break;
                    case 35: // End
                        if (bOpen) {
                            toggleHover($current, 0);
                            toggleHover($items.eq(nLastIndex), 1);
                        }
                        break;
                    case 36: // Home
                        if (bOpen) {
                            toggleHover($current, 0);
                            toggleHover($items.eq(0), 1);
                        }
                        break;
                    case 38: // Up
                        if (bOpen) {
                            toggleHover($current, 0);
                            // If not already key-navigated or first item is selected, cycle to the last item; or
                            // else select the previous item
                            toggleHover(nHoverIndex ? $items.eq(nHoverIndex-1) : $items.eq(nLastIndex), 1);
                        }
                        break;
                    case 40: // Down
                        if (bOpen) {
                            toggleHover($current, 0);
                            // If last item is selected, cycle to the first item; or else select the next item
                            toggleHover(nHoverIndex===nLastIndex ? $items.eq(0) : $items.eq(nHoverIndex+1), 1);
                        }
                        break;
                    default:
                        if (bOpen) sKey = aKeys[e.which-48];
                }
                if (sKey) { // Alphanumeric key pressed
                    clearTimeout(nTimer);
                    $dropdown.data('keysPressed', $dropdown.data('keysPressed')===undefined ? sKey : $dropdown.data('keysPressed') + sKey);
                    nTimer = setTimeout(function() {
                        $dropdown.removeData('keysPressed');
                        // NOTE: Windows keyboard repeat delay is 250-1000 ms. See
                        // https://technet.microsoft.com/en-us/library/cc978658.aspx
                    }, 300);
                    // Build index of matches
                    var aMatches = [],
                        nCurrentIndex = $current.index();
                    $items.each(function(nIndex) {
                        if ($(this).text().toLowerCase().indexOf($dropdown.data('keysPressed'))===0) aMatches.push(nIndex);
                    });
                    if (aMatches.length) {
                        // Cycle through items matching key(s) pressed
                        for (var i=0; i<aMatches.length; ++i) {
                            if (aMatches[i]>nCurrentIndex) {
                                toggleHover($items, 0);
                                toggleHover($items.eq(aMatches[i]), 1);
                                break;
                            }
                            if (i===aMatches.length-1) {
                                toggleHover($items, 0);
                                toggleHover($items.eq(aMatches[0]), 1);
                            }
                        }
                    }
                }
            },

            // Highlight menu item
            hoverDropdownItem = function(e) {
                var $dropdown = $(e.currentTarget);
                if (e.target.nodeName!=='LI' || !$dropdown.hasClass('active') || new Date()-$dropdown.data('lastKeypress')<200) return;
                toggleHover($dropdown.children(), 0, 1);
                toggleHover($(e.target), 1, 1);
            },

            // Construct menu item
            // elOpt is null for first item in multi-select menus
            renderItem = function(elOpt, sClass, bSelected) {
                var sGroup = '',
                    sText = '',
                    sTitle;
                sClass = sClass || '';
                if (elOpt) {
                    switch (elOpt.nodeName) {
                        case 'OPTION':
                            if (elOpt.parentNode.nodeName==='OPTGROUP') sGroup = elOpt.parentNode.getAttribute('label');
                            sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.text + (elOpt.getAttribute('data-suffix') || '');
                            break;
                        case 'OPTGROUP':
                            sClass += ' label';
                            sText = (elOpt.getAttribute('data-prefix') || '') + elOpt.getAttribute('label') + (elOpt.getAttribute('data-suffix') || '');
                            break;
                    }
                    if (elOpt.disabled || (sGroup && elOpt.parentNode.disabled)) sClass += ' disabled';
                    sTitle = elOpt.title;
                    if (sGroup && !sTitle) sTitle = elOpt.parentNode.title;
                }
                ++nCount;
                return '<li id="item' + nTimestamp + '-' + nCount + '"'
                    + (sGroup ? ' data-group="' + sGroup + '"' : '')
                    + (elOpt && (elOpt.value||oOptions.classic) ? ' data-value="' + elOpt.value + '"' : '')
                    + (elOpt && elOpt.nodeName==='OPTION' ? ' role="option"' : '')
                    + (sTitle ? ' title="' + sTitle + '" aria-label="' + sTitle + '"' : '')
                    + (sClass ? ' class="' + $.trim(sClass) + '"' : '')
                    + ((oOptions.height!==50) ? ' style="height:' + (oOptions.height-2)
                        + 'px;line-height:' + (oOptions.height-4) + 'px;"' : '') + '>' + sText
                    + ((bSelected || sClass==='selected') ? oOptions.selectedMarker : '') + '</li>';
            },

            // Reset menu state
            // @param o Event or Element object
            resetDropdown = function(o) {
                if (oOptions.hoverIntent<0 && o.type==='mouseleave') return;
                var $dropdown = $(o.currentTarget||o);
                $dropdown.data('hover', false);
                clearTimeout(nTimer);
                nTimer = setTimeout(function() {
                    if ($dropdown.data('hover')) return;
                    if ($dropdown.hasClass('reverse') && !oOptions.classic) $dropdown.prepend($dropdown.children(':last-child'));
                    $dropdown.removeClass('active reverse').removeData('clicked').attr('aria-expanded', 'false').css('height', '');
                    $dropdown.children().removeClass('hover nohover');
                    // Update focus for NVDA screen readers
                    $dropdown.attr('aria-activedescendant', $dropdown.children('.selected').attr('id'));
                }, (o.type==='mouseleave' && !$dropdown.data('clicked')) ? oOptions.hoverIntent : 0);
            },

            // Set menu item hover state
            // bNoScroll set on hoverDropdownItem()
            toggleHover = function($li, bOn, bNoScroll) {
                if (bOn) {
                    var $dropdown = $li.parent();
                    $li.removeClass('nohover').addClass('hover');
                    // Update focus for NVDA screen readers
                    $dropdown.attr('aria-activedescendant', $li.attr('id'));
                    if ($li.length===1 && $current && !bNoScroll) {
                        // Ensure items are always in view
                        var nDropdownHeight = $dropdown.outerHeight(),
                            nItemOffset = $li.offset().top-$dropdown.offset().top-1; // -1px for top border
                        if ($li.index()===0) {
                            $dropdown.scrollTop(0);
                        } else if ($li.index()===nLastIndex) {
                            $dropdown.scrollTop($dropdown.children().length*oOptions.height);
                        } else {
                            if (nItemOffset+oOptions.height>nDropdownHeight) $dropdown.scrollTop($dropdown.scrollTop()+oOptions.height+nItemOffset-nDropdownHeight);
                            else if (nItemOffset<0) $dropdown.scrollTop($dropdown.scrollTop()+nItemOffset);
                        }
                    }
                } else {
                    $li.removeClass('hover').addClass('nohover');
                }
            },

            // Update selected values for multi-select menu
            updateSelected = function($dropdown) {
                var $select = $dropdown.parent().children('select'),
                    aSelected = $('option', $select).map(function() {
                        if (this.selected) return this.text;
                    }).get(),
                    sSelected;
                if (oOptions.multiVerbosity>=aSelected.length) sSelected = aSelected.join(oOptions.multiDelimiter) || MULTI_NONE;
                else sSelected = aSelected.length + '/' + $('option', $select).length + MULTI_POSTFIX;
                if (sSelected) {
                    var sTitle = ($select.attr('title') ? $select.attr('title') : '') + (aSelected.length ? '\n' + MULTI_PREFIX + aSelected.join(oOptions.multiDelimiter) : '');
                    $dropdown.children('.selected').text(sSelected);
                    $dropdown.attr({
                        'title': sTitle,
                        'aria-label': sTitle
                    });
                } else {
                    $dropdown.children('.selected').empty();
                    $dropdown.attr({
                        'title': $select.attr('title'),
                        'aria-label': $select.attr('title')
                    });
                }
            };

        /**
         * Public Functions
         */

        // Resync the menu with <select> to reflect state changes
        this.refresh = function(oOptions) {
            return this.each(function() {
                var $select = $(this);
                $select.prevAll('ul').remove();
                $select.unwrap().data('loaded', false);
                this.size = $select.data('size');
                init(this);
            });
        };

        return this.each(function() {
            init(this);
        });

    };
}(jQuery));