UNPKG

materialize-css

Version:

Builds Materialize distribution packages

618 lines (540 loc) 18.4 kB
(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 <a> and <button> let focusedElement = this.dropdownEl.children[this.focusedIndex]; let $activatableElement = $(focusedElement) .find('a, button') .first(); // Click a or button tag if exists, otherwise click li tag if (!!$activatableElement.length) { $activatableElement[0].click(); } else if (!!focusedElement) { focusedElement.click(); } // Close dropdown on ESC } else if (e.which === M.keys.ESC && this.isOpen) { e.preventDefault(); this.close(); } // CASE WHEN USER TYPE LETTERS let letter = String.fromCharCode(e.which).toLowerCase(), nonLetters = [9, 13, 27, 38, 40]; if (letter && nonLetters.indexOf(e.which) === -1) { this.filterQuery.push(letter); let string = this.filterQuery.join(''), newOptionEl = $(this.dropdownEl) .find('li') .filter((el) => { return ( $(el) .text() .toLowerCase() .indexOf(string) === 0 ); })[0]; if (newOptionEl) { this.focusedIndex = $(newOptionEl).index(); this._focusFocusedItem(); } } this.filterTimeout = setTimeout(this._resetFilterQueryBound, 1000); } /** * Setup dropdown */ _resetFilterQuery() { this.filterQuery = []; } _resetDropdownStyles() { this.$dropdownEl.css({ display: '', width: '', height: '', left: '', top: '', 'transform-origin': '', transform: '', opacity: '' }); } _makeDropdownFocusable() { // Needed for arrow key navigation this.dropdownEl.tabIndex = 0; // Only set tabindex if it hasn't been set by user $(this.dropdownEl) .children() .each(function(el) { if (!el.getAttribute('tabindex')) { el.setAttribute('tabindex', 0); } }); } _focusFocusedItem() { if ( this.focusedIndex >= 0 && this.focusedIndex < this.dropdownEl.children.length && this.options.autoFocus ) { this.dropdownEl.children[this.focusedIndex].focus(); } } _getDropdownPosition() { let offsetParentBRect = this.el.offsetParent.getBoundingClientRect(); let triggerBRect = this.el.getBoundingClientRect(); let dropdownBRect = this.dropdownEl.getBoundingClientRect(); let idealHeight = dropdownBRect.height; let idealWidth = dropdownBRect.width; let idealXPos = triggerBRect.left - dropdownBRect.left; let idealYPos = triggerBRect.top - dropdownBRect.top; let dropdownBounds = { left: idealXPos, top: idealYPos, height: idealHeight, width: idealWidth }; // Countainer here will be closest ancestor with overflow: hidden let closestOverflowParent = !!this.dropdownEl.offsetParent ? this.dropdownEl.offsetParent : this.dropdownEl.parentNode; let alignments = M.checkPossibleAlignments( this.el, closestOverflowParent, dropdownBounds, this.options.coverTrigger ? 0 : triggerBRect.height ); let verticalAlignment = 'top'; let horizontalAlignment = this.options.alignment; idealYPos += this.options.coverTrigger ? 0 : triggerBRect.height; // Reset isScrollable this.isScrollable = false; if (!alignments.top) { if (alignments.bottom) { verticalAlignment = 'bottom'; } else { this.isScrollable = true; // Determine which side has most space and cutoff at correct height if (alignments.spaceOnTop > alignments.spaceOnBottom) { verticalAlignment = 'bottom'; idealHeight += alignments.spaceOnTop; idealYPos -= alignments.spaceOnTop; } else { idealHeight += alignments.spaceOnBottom; } } } // If preferred horizontal alignment is possible if (!alignments[horizontalAlignment]) { let oppositeAlignment = horizontalAlignment === 'left' ? 'right' : 'left'; if (alignments[oppositeAlignment]) { horizontalAlignment = oppositeAlignment; } else { // Determine which side has most space and cutoff at correct height if (alignments.spaceOnLeft > alignments.spaceOnRight) { horizontalAlignment = 'right'; idealWidth += alignments.spaceOnLeft; idealXPos -= alignments.spaceOnLeft; } else { horizontalAlignment = 'left'; idealWidth += alignments.spaceOnRight; } } } if (verticalAlignment === 'bottom') { idealYPos = idealYPos - dropdownBRect.height + (this.options.coverTrigger ? triggerBRect.height : 0); } if (horizontalAlignment === 'right') { idealXPos = idealXPos - dropdownBRect.width + triggerBRect.width; } return { x: idealXPos, y: idealYPos, verticalAlignment: verticalAlignment, horizontalAlignment: horizontalAlignment, height: idealHeight, width: idealWidth }; } /** * Animate in dropdown */ _animateIn() { anim.remove(this.dropdownEl); anim({ targets: this.dropdownEl, opacity: { value: [0, 1], easing: 'easeOutQuad' }, scaleX: [0.3, 1], scaleY: [0.3, 1], duration: this.options.inDuration, easing: 'easeOutQuint', complete: (anim) => { if (this.options.autoFocus) { this.dropdownEl.focus(); } // onOpenEnd callback if (typeof this.options.onOpenEnd === 'function') { this.options.onOpenEnd.call(this, this.el); } } }); } /** * Animate out dropdown */ _animateOut() { anim.remove(this.dropdownEl); anim({ targets: this.dropdownEl, opacity: { value: 0, easing: 'easeOutQuint' }, scaleX: 0.3, scaleY: 0.3, duration: this.options.outDuration, easing: 'easeOutQuint', complete: (anim) => { this._resetDropdownStyles(); // onCloseEnd callback if (typeof this.options.onCloseEnd === 'function') { this.options.onCloseEnd.call(this, this.el); } } }); } /** * Place dropdown */ _placeDropdown() { // Set width before calculating positionInfo let idealWidth = this.options.constrainWidth ? this.el.getBoundingClientRect().width : this.dropdownEl.getBoundingClientRect().width; this.dropdownEl.style.width = idealWidth + 'px'; let positionInfo = this._getDropdownPosition(); this.dropdownEl.style.left = positionInfo.x + 'px'; this.dropdownEl.style.top = positionInfo.y + 'px'; this.dropdownEl.style.height = positionInfo.height + 'px'; this.dropdownEl.style.width = positionInfo.width + 'px'; this.dropdownEl.style.transformOrigin = `${ positionInfo.horizontalAlignment === 'left' ? '0' : '100%' } ${positionInfo.verticalAlignment === 'top' ? '0' : '100%'}`; } /** * Open Dropdown */ open() { if (this.isOpen) { return; } this.isOpen = true; // onOpenStart callback if (typeof this.options.onOpenStart === 'function') { this.options.onOpenStart.call(this, this.el); } // Reset styles this._resetDropdownStyles(); this.dropdownEl.style.display = 'block'; this._placeDropdown(); this._animateIn(); this._setupTemporaryEventHandlers(); } /** * Close Dropdown */ close() { if (!this.isOpen) { return; } this.isOpen = false; this.focusedIndex = -1; // onCloseStart callback if (typeof this.options.onCloseStart === 'function') { this.options.onCloseStart.call(this, this.el); } this._animateOut(); this._removeTemporaryEventHandlers(); if (this.options.autoFocus) { this.el.focus(); } } /** * Recalculate dimensions */ recalculateDimensions() { if (this.isOpen) { this.$dropdownEl.css({ width: '', height: '', left: '', top: '', 'transform-origin': '' }); this._placeDropdown(); } } } /** * @static * @memberof Dropdown */ Dropdown._dropdowns = []; M.Dropdown = Dropdown; if (M.jQueryLoaded) { M.initializeJqueryWrapper(Dropdown, 'dropdown', 'M_Dropdown'); } })(cash, M.anime);