dropdown.js 10.49 KiB
/* global Popper */
import Util from './util'
/**
 * --------------------------------------------------------------------------
 * Bootstrap (v4.0.0-alpha.6): dropdown.js
 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
 * --------------------------------------------------------------------------
const Dropdown = (($) => {
  /**
   * Check for Popper dependency
   * Popper - https://popper.js.org
  if (typeof Popper === 'undefined') {
    throw new Error('Bootstrap dropdown require Popper.js (https://popper.js.org)')
  /**
   * ------------------------------------------------------------------------
   * Constants
   * ------------------------------------------------------------------------
  const NAME                     = 'dropdown'
  const VERSION                  = '4.0.0-alpha.6'
  const DATA_KEY                 = 'bs.dropdown'
  const EVENT_KEY                = `.${DATA_KEY}`
  const DATA_API_KEY             = '.data-api'
  const JQUERY_NO_CONFLICT       = $.fn[NAME]
  const ESCAPE_KEYCODE           = 27 // KeyboardEvent.which value for Escape (Esc) key
  const SPACE_KEYCODE            = 32 // KeyboardEvent.which value for space key
  const TAB_KEYCODE              = 9 // KeyboardEvent.which value for tab key
  const ARROW_UP_KEYCODE         = 38 // KeyboardEvent.which value for up arrow key
  const ARROW_DOWN_KEYCODE       = 40 // KeyboardEvent.which value for down arrow key
  const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)
  const REGEXP_KEYDOWN           = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)
  const Event = {
    HIDE             : `hide${EVENT_KEY}`,
    HIDDEN           : `hidden${EVENT_KEY}`,
    SHOW             : `show${EVENT_KEY}`,
    SHOWN            : `shown${EVENT_KEY}`,
    CLICK            : `click${EVENT_KEY}`,
    CLICK_DATA_API   : `click${EVENT_KEY}${DATA_API_KEY}`,
    KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,
    KEYUP_DATA_API   : `keyup${EVENT_KEY}${DATA_API_KEY}`
  const ClassName = {
    DISABLED : 'disabled',
    SHOW     : 'show'
  const Selector = {
    DATA_TOGGLE   : '[data-toggle="dropdown"]',
    FORM_CHILD    : '.dropdown form',
    MENU          : '.dropdown-menu',
    NAVBAR_NAV    : '.navbar-nav',
    VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled)'
  const AttachmentMap = {
    TOP    : 'top-start',
    BOTTOM : 'bottom-start'
7172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
const Default = { placement : AttachmentMap.BOTTOM, offset : 0 } const DefaultType = { placement : 'string', offset : '(number|string)' } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ class Dropdown { constructor(element, config) { this._element = element this._popper = null this._config = this._getConfig(config) this._menu = this._getMenuElement() this._addEventListeners() } // getters static get VERSION() { return VERSION } static get Default() { return Default } static get DefaultType() { return DefaultType } // public toggle() { let context = $(this).data(DATA_KEY) if (!context) { context = new Dropdown(this) $(this).data(DATA_KEY, context) } if (context.disabled || $(this).hasClass(ClassName.DISABLED)) { return false } const parent = Dropdown._getParentFromElement(this) const isActive = $(context._menu).hasClass(ClassName.SHOW) Dropdown._clearMenus() if (isActive) { return false } const relatedTarget = { relatedTarget : this }
141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
const showEvent = $.Event(Event.SHOW, relatedTarget) $(parent).trigger(showEvent) if (showEvent.isDefaultPrevented()) { return false } // Handle dropup const dropdownPlacement = $(this).parent().hasClass('dropup') ? AttachmentMap.TOP : context._config.placement this._popper = new Popper(this, context._menu, { placement : dropdownPlacement, modifiers : { offset : { offset : context._config.offset } } }) // if this is a touch-enabled device we add extra // empty mouseover listeners to the body's immediate children; // only needed because of broken event delegation on iOS // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html if ('ontouchstart' in document.documentElement && !$(parent).closest(Selector.NAVBAR_NAV).length) { $('body').children().on('mouseover', null, $.noop) } this.focus() this.setAttribute('aria-expanded', true) $(context._menu).toggleClass(ClassName.SHOW) $(parent) .toggleClass(ClassName.SHOW) .trigger($.Event(Event.SHOWN, relatedTarget)) return false } dispose() { $.removeData(this._element, DATA_KEY) $(this._element).off(EVENT_KEY) this._element = null this._menu = null if (this._popper !== null) { this._popper.destroy() } } // private _addEventListeners() { $(this._element).on(Event.CLICK, this.toggle) } _getConfig(config) { config = $.extend( {}, this.constructor.Default, $(this._element).data(), config ) Util.typeCheckConfig( NAME, config, this.constructor.DefaultType )
211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
return config } _getMenuElement() { if (!this._menu) { const parent = Dropdown._getParentFromElement(this._element) this._menu = $(parent).find(Selector.MENU)[0] } return this._menu } // static static _jQueryInterface(config) { return this.each(function () { let data = $(this).data(DATA_KEY) const _config = typeof config === 'object' ? config : null if (!data) { data = new Dropdown(this, _config) $(this).data(DATA_KEY, data) } if (typeof config === 'string') { if (data[config] === undefined) { throw new Error(`No method named "${config}"`) } data[config].call(this) } }) } static _clearMenus(event) { if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH || event.type === 'keyup' && event.which !== TAB_KEYCODE)) { return } const toggles = $.makeArray($(Selector.DATA_TOGGLE)) for (let i = 0; i < toggles.length; i++) { const parent = Dropdown._getParentFromElement(toggles[i]) const context = $(toggles[i]).data(DATA_KEY) const relatedTarget = { relatedTarget : toggles[i] } if (!context) { continue } const dropdownMenu = context._menu if (!$(parent).hasClass(ClassName.SHOW)) { continue } if (event && (event.type === 'click' && /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) && $.contains(parent, event.target)) { continue } const hideEvent = $.Event(Event.HIDE, relatedTarget) $(parent).trigger(hideEvent) if (hideEvent.isDefaultPrevented()) { continue } // if this is a touch-enabled device we remove the extra // empty mouseover listeners we added for iOS support if ('ontouchstart' in document.documentElement) {
281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
$('body').children().off('mouseover', null, $.noop) } toggles[i].setAttribute('aria-expanded', 'false') $(dropdownMenu).removeClass(ClassName.SHOW) $(parent) .removeClass(ClassName.SHOW) .trigger($.Event(Event.HIDDEN, relatedTarget)) } } static _getParentFromElement(element) { let parent const selector = Util.getSelectorFromElement(element) if (selector) { parent = $(selector)[0] } return parent || element.parentNode } static _dataApiKeydownHandler(event) { if (!REGEXP_KEYDOWN.test(event.which) || /button/i.test(event.target.tagName) && event.which === SPACE_KEYCODE || /input|textarea/i.test(event.target.tagName)) { return } event.preventDefault() event.stopPropagation() if (this.disabled || $(this).hasClass(ClassName.DISABLED)) { return } const parent = Dropdown._getParentFromElement(this) const isActive = $(parent).hasClass(ClassName.SHOW) if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) { if (event.which === ESCAPE_KEYCODE) { const toggle = $(parent).find(Selector.DATA_TOGGLE)[0] $(toggle).trigger('focus') } $(this).trigger('click') return } const items = $(parent).find(Selector.VISIBLE_ITEMS).get() if (!items.length) { return } let index = items.indexOf(event.target) if (event.which === ARROW_UP_KEYCODE && index > 0) { // up index-- } if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // down index++ } if (index < 0) { index = 0 }
351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
items[index].focus() } } /** * ------------------------------------------------------------------------ * Data Api implementation * ------------------------------------------------------------------------ */ $(document) .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler) .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler) .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus) .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, Dropdown.prototype.toggle) .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => { e.stopPropagation() }) /** * ------------------------------------------------------------------------ * jQuery * ------------------------------------------------------------------------ */ $.fn[NAME] = Dropdown._jQueryInterface $.fn[NAME].Constructor = Dropdown $.fn[NAME].noConflict = function () { $.fn[NAME] = JQUERY_NO_CONFLICT return Dropdown._jQueryInterface } return Dropdown })(jQuery) export default Dropdown