diff options
Diffstat (limited to 'node_modules/materialize-css/js/dropdown.js')
-rw-r--r-- | node_modules/materialize-css/js/dropdown.js | 617 |
1 files changed, 617 insertions, 0 deletions
diff --git a/node_modules/materialize-css/js/dropdown.js b/node_modules/materialize-css/js/dropdown.js new file mode 100644 index 0000000000..899bba3a3b --- /dev/null +++ b/node_modules/materialize-css/js/dropdown.js @@ -0,0 +1,617 @@ +(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); |