(function($, anim) { 'use strict'; let _defaults = { alignment: 'left', autoFocus: true, constrainWidth: true, container: null, coverTrigger: true, closeOnClick: true, hover: false, inDuration: 150, outDuration: 250, onOpenStart: null, onOpenEnd: null, onCloseStart: null, onCloseEnd: null, onItemClick: null }; /** * @class */ class Dropdown extends Component { constructor(el, options) { super(Dropdown, el, options); this.el.M_Dropdown = this; Dropdown._dropdowns.push(this); this.id = M.getIdFromTrigger(el); this.dropdownEl = document.getElementById(this.id); this.$dropdownEl = $(this.dropdownEl); /** * Options for the dropdown * @member Dropdown#options * @prop {String} [alignment='left'] - Edge which the dropdown is aligned to * @prop {Boolean} [autoFocus=true] - Automatically focus dropdown el for keyboard * @prop {Boolean} [constrainWidth=true] - Constrain width to width of the button * @prop {Element} container - Container element to attach dropdown to (optional) * @prop {Boolean} [coverTrigger=true] - Place dropdown over trigger * @prop {Boolean} [closeOnClick=true] - Close on click of dropdown item * @prop {Boolean} [hover=false] - Open dropdown on hover * @prop {Number} [inDuration=150] - Duration of open animation in ms * @prop {Number} [outDuration=250] - Duration of close animation in ms * @prop {Function} onOpenStart - Function called when dropdown starts opening * @prop {Function} onOpenEnd - Function called when dropdown finishes opening * @prop {Function} onCloseStart - Function called when dropdown starts closing * @prop {Function} onCloseEnd - Function called when dropdown finishes closing */ this.options = $.extend({}, Dropdown.defaults, options); /** * Describes open/close state of dropdown * @type {Boolean} */ this.isOpen = false; /** * Describes if dropdown content is scrollable * @type {Boolean} */ this.isScrollable = false; /** * Describes if touch moving on dropdown content * @type {Boolean} */ this.isTouchMoving = false; this.focusedIndex = -1; this.filterQuery = []; // Move dropdown-content after dropdown-trigger if (!!this.options.container) { $(this.options.container).append(this.dropdownEl); } else { this.$el.after(this.dropdownEl); } this._makeDropdownFocusable(); this._resetFilterQueryBound = this._resetFilterQuery.bind(this); this._handleDocumentClickBound = this._handleDocumentClick.bind(this); this._handleDocumentTouchmoveBound = this._handleDocumentTouchmove.bind(this); this._handleDropdownClickBound = this._handleDropdownClick.bind(this); this._handleDropdownKeydownBound = this._handleDropdownKeydown.bind(this); this._handleTriggerKeydownBound = this._handleTriggerKeydown.bind(this); this._setupEventHandlers(); } static get defaults() { return _defaults; } static init(els, options) { return super.init(this, els, options); } /** * Get Instance */ static getInstance(el) { let domElem = !!el.jquery ? el[0] : el; return domElem.M_Dropdown; } /** * Teardown component */ destroy() { this._resetDropdownStyles(); this._removeEventHandlers(); Dropdown._dropdowns.splice(Dropdown._dropdowns.indexOf(this), 1); this.el.M_Dropdown = undefined; } /** * Setup Event Handlers */ _setupEventHandlers() { // Trigger keydown handler this.el.addEventListener('keydown', this._handleTriggerKeydownBound); // Item click handler this.dropdownEl.addEventListener('click', this._handleDropdownClickBound); // Hover event handlers if (this.options.hover) { this._handleMouseEnterBound = this._handleMouseEnter.bind(this); this.el.addEventListener('mouseenter', this._handleMouseEnterBound); this._handleMouseLeaveBound = this._handleMouseLeave.bind(this); this.el.addEventListener('mouseleave', this._handleMouseLeaveBound); this.dropdownEl.addEventListener('mouseleave', this._handleMouseLeaveBound); // Click event handlers } else { this._handleClickBound = this._handleClick.bind(this); this.el.addEventListener('click', this._handleClickBound); } } /** * Remove Event Handlers */ _removeEventHandlers() { this.el.removeEventListener('keydown', this._handleTriggerKeydownBound); this.dropdownEl.removeEventListener('click', this._handleDropdownClickBound); if (this.options.hover) { this.el.removeEventListener('mouseenter', this._handleMouseEnterBound); this.el.removeEventListener('mouseleave', this._handleMouseLeaveBound); this.dropdownEl.removeEventListener('mouseleave', this._handleMouseLeaveBound); } else { this.el.removeEventListener('click', this._handleClickBound); } } _setupTemporaryEventHandlers() { // Use capture phase event handler to prevent click document.body.addEventListener('click', this._handleDocumentClickBound, true); document.body.addEventListener('touchend', this._handleDocumentClickBound); document.body.addEventListener('touchmove', this._handleDocumentTouchmoveBound); this.dropdownEl.addEventListener('keydown', this._handleDropdownKeydownBound); } _removeTemporaryEventHandlers() { // Use capture phase event handler to prevent click document.body.removeEventListener('click', this._handleDocumentClickBound, true); document.body.removeEventListener('touchend', this._handleDocumentClickBound); document.body.removeEventListener('touchmove', this._handleDocumentTouchmoveBound); this.dropdownEl.removeEventListener('keydown', this._handleDropdownKeydownBound); } _handleClick(e) { e.preventDefault(); this.open(); } _handleMouseEnter() { this.open(); } _handleMouseLeave(e) { let toEl = e.toElement || e.relatedTarget; let leaveToDropdownContent = !!$(toEl).closest('.dropdown-content').length; let leaveToActiveDropdownTrigger = false; let $closestTrigger = $(toEl).closest('.dropdown-trigger'); if ( $closestTrigger.length && !!$closestTrigger[0].M_Dropdown && $closestTrigger[0].M_Dropdown.isOpen ) { leaveToActiveDropdownTrigger = true; } // Close hover dropdown if mouse did not leave to either active dropdown-trigger or dropdown-content if (!leaveToActiveDropdownTrigger && !leaveToDropdownContent) { this.close(); } } _handleDocumentClick(e) { let $target = $(e.target); if ( this.options.closeOnClick && $target.closest('.dropdown-content').length && !this.isTouchMoving ) { // isTouchMoving to check if scrolling on mobile. setTimeout(() => { this.close(); }, 0); } else if ( $target.closest('.dropdown-trigger').length || !$target.closest('.dropdown-content').length ) { setTimeout(() => { this.close(); }, 0); } this.isTouchMoving = false; } _handleTriggerKeydown(e) { // ARROW DOWN OR ENTER WHEN SELECT IS CLOSED - open Dropdown if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ENTER) && !this.isOpen) { e.preventDefault(); this.open(); } } /** * Handle Document Touchmove * @param {Event} e */ _handleDocumentTouchmove(e) { let $target = $(e.target); if ($target.closest('.dropdown-content').length) { this.isTouchMoving = true; } } /** * Handle Dropdown Click * @param {Event} e */ _handleDropdownClick(e) { // onItemClick callback if (typeof this.options.onItemClick === 'function') { let itemEl = $(e.target).closest('li')[0]; this.options.onItemClick.call(this, itemEl); } } /** * Handle Dropdown Keydown * @param {Event} e */ _handleDropdownKeydown(e) { if (e.which === M.keys.TAB) { e.preventDefault(); this.close(); // Navigate down dropdown list } else if ((e.which === M.keys.ARROW_DOWN || e.which === M.keys.ARROW_UP) && this.isOpen) { e.preventDefault(); let direction = e.which === M.keys.ARROW_DOWN ? 1 : -1; let newFocusedIndex = this.focusedIndex; let foundNewIndex = false; do { newFocusedIndex = newFocusedIndex + direction; if ( !!this.dropdownEl.children[newFocusedIndex] && this.dropdownEl.children[newFocusedIndex].tabIndex !== -1 ) { foundNewIndex = true; break; } } while (newFocusedIndex < this.dropdownEl.children.length && newFocusedIndex >= 0); if (foundNewIndex) { this.focusedIndex = newFocusedIndex; this._focusFocusedItem(); } // ENTER selects choice on focused item } else if (e.which === M.keys.ENTER && this.isOpen) { // Search for and