import { createEvents } from '@/core/modules/createEvents'
import eventPromise from '@/utils/eventPromise'
import animationEnd from '@/utils/animationEnd'
import mitt from 'mitt'
/** *
* @namespace Dropdown
* @class Dropdown
* @description Handles dropdowns
*
* @example
*
* Required markup:
*
* html:
* <div data-ui="Dropdown" data-ui-options='{"update-text": true, "intercept-links": true}' data-ui-key="downloads-dropdown">
* <button data-dropdown-button>
* <span class="mr-1" data-dropdown-text>View all products</span>
* </button>
* <div data-dropdown-menu>
* <ul class="list-reset">
* <li data-dropdown-reset role="button" tabindex="0">Reset</li>
* <li>
* <a data-dropdown-item>item</a>
* </li>
* <li>
* <a data-dropdown-item>item</a>
* </li>
* <li>
* <a data-dropdown-item>item</a>
* </li>
* </ul>
* </div>
* </div>
*
* @param {HTMLElement} el - node to bind to
* @param {Object} options - options
* @param {String} key - key name
*
* @property {Boolean} options.updateText - should the dropdown text update to reflect the selected item
* @property {Boolean} options.interceptLinks - should any links called prevent default
* @property {Boolean} options.closeOnClick - should the dropdown close when an item is clicked
*
* @return {DropDown}
*/
export default class DropDown {
defaults = {
updateText: false,
interceptLinks: false,
closeOnClick: true
}
constructor(el, options, key) {
this.options = { ...this.defaults, ...options }
this.key = key
Object.assign(this, mitt())
this.$el = el
// bind the dom events
this.$$events = createEvents.call(this, this.$el, this.events)
this.$reset = this.$el.querySelector('[data-dropdown-reset]')
this.$label = this.$el.querySelector('[data-dropdown-text]')
this.$button = this.$el.querySelector('[data-dropdown-button]')
this.$dropdown = this.$el.querySelector('[data-dropdown-menu]')
this.$items = [...this.$el.querySelectorAll('[data-dropdown-item]')]
this.originalText = this.$label.textContent
// the state machine... will always invoke the next stage of state
// https://en.wikipedia.org/wiki/Finite-state_machine
this.machine = {
open: { CLICK: 'close' },
close: { CLICK: 'open' }
}
// set the inital state
this.state = 'close'
this.$selectedItem = null
}
/** *
* @memberof Dropdown
* @method mount
* @desc Add the events
*
* @return {void}
*/
mount = () => {
this.$$events.attachAll()
}
/** *
* @method mount
* @memberof Dropdown
* @desc removes the events
*
* @return {void}
*/
unmount = () => {
this.$$events.destroy()
this.off('*')
}
events = {
'click [data-dropdown-button]': 'onClick',
'blur [data-dropdown-item]': 'onBlur',
'click [data-dropdown-item]': 'onItemClick',
'click [data-dropdown-reset]': 'onResetClick'
}
/** *
* @memberof Dropdown
* @method onClick
* @desc the click event...
* @param {Object} e : the event object
*
* @return {void}
*/
onClick = e => {
e.preventDefault()
const action = this.machine[this.state].CLICK
this[action]()
this.state = action
}
/** *
* @memberof Dropdown
* @method onBlur
* @desc the blur event... this is used to close the dropdown when clicking outside
*
* @return {void}
*/
onBlur = () => {
// must be wrapped in a setTimeout
setTimeout(() => {
// if the active node is the button, do nothing
if (document.activeElement === this.$button) return
// if the active node is outside of the dropdown
// close it, reset the state
if (!document.activeElement.closest('[data-dropdown-menu]')) {
this.state = 'close'
this.close()
}
})
}
/** *
* @memberof Dropdown
* @method onItemClick
* @desc handle various options, this is only called when intercept links is true
* @param {Object} e : the event object
* @param {HTMLElement} elm : the element clicked
*
* @return {void}
*/
onItemClick = (e, elm) => {
const { interceptLinks, updateText, closeOnClick } = this.options
if (this.$selectedItem && this.$selectedItem !== elm) {
this.$selectedItem.classList.remove('is-selected')
}
if (interceptLinks) {
e.preventDefault()
this.emit('dropdown:item:clicked', { elm, key: this.key })
}
if (updateText) {
const text = elm.textContent.trim()
this.$label.textContent = text
}
if (closeOnClick) {
this.close().then(() => {
if (updateText) {
this.$reset.style.display = 'block'
}
})
}
elm.classList.add('is-selected')
this.$selectedItem = elm
}
/** *
* @memberof Dropdown
* @method onResetClick
* @desc reset the dropdown label text and close
* @param {Object} e : the event object
*
* @return {void}
*/
onResetClick = e => {
e.preventDefault()
this.$label.textContent = this.originalText
if (this.$selectedItem) {
this.$selectedItem.classList.remove('is-selected')
}
this.close().then(() => {
this.$reset.style.display = ''
})
}
/** *
* @memberof Dropdown
* @method open
* @desc open the dropdown, once the animation has finished set the focus state
*
* @return {Promise}
*/
open = () => {
this.$button.classList.add('is-active')
return eventPromise(animationEnd('transition'), this.$dropdown, () => {
this.$dropdown.classList.add('is-open')
}).then(() => {
this.$items[0].focus()
})
}
/** *
* @memberof Dropdown
* @method close
* @desc close the dropdown
*
* @return {void}
*/
close = () =>
eventPromise(animationEnd('transition'), this.$dropdown, () => {
this.$button.classList.remove('is-active')
this.$dropdown.classList.remove('is-open')
this.state = 'close'
})
}