2 import Popper from 'popper.js'
3 import Util from './util'
6 * --------------------------------------------------------------------------
7 * Bootstrap (v4.1.3): dropdown.js
8 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
9 * --------------------------------------------------------------------------
12 const Dropdown = (($) => {
14 * ------------------------------------------------------------------------
16 * ------------------------------------------------------------------------
19 const NAME = 'dropdown'
20 const VERSION = '4.1.3'
21 const DATA_KEY = 'bs.dropdown'
22 const EVENT_KEY = `.${DATA_KEY}`
23 const DATA_API_KEY = '.data-api'
24 const JQUERY_NO_CONFLICT = $.fn[NAME]
25 const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
26 const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key
27 const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key
28 const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key
29 const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key
30 const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)
31 const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)
34 HIDE : `hide${EVENT_KEY}`,
35 HIDDEN : `hidden${EVENT_KEY}`,
36 SHOW : `show${EVENT_KEY}`,
37 SHOWN : `shown${EVENT_KEY}`,
38 CLICK : `click${EVENT_KEY}`,
39 CLICK_DATA_API : `click${EVENT_KEY}${DATA_API_KEY}`,
40 KEYDOWN_DATA_API : `keydown${EVENT_KEY}${DATA_API_KEY}`,
41 KEYUP_DATA_API : `keyup${EVENT_KEY}${DATA_API_KEY}`
45 DISABLED : 'disabled',
48 DROPRIGHT : 'dropright',
49 DROPLEFT : 'dropleft',
50 MENURIGHT : 'dropdown-menu-right',
51 MENULEFT : 'dropdown-menu-left',
52 POSITION_STATIC : 'position-static'
56 DATA_TOGGLE : '[data-toggle="dropdown"]',
57 FORM_CHILD : '.dropdown form',
58 MENU : '.dropdown-menu',
59 NAVBAR_NAV : '.navbar-nav',
60 VISIBLE_ITEMS : '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
63 const AttachmentMap = {
66 BOTTOM : 'bottom-start',
67 BOTTOMEND : 'bottom-end',
68 RIGHT : 'right-start',
69 RIGHTEND : 'right-end',
77 boundary : 'scrollParent',
83 offset : '(number|string|function)',
85 boundary : '(string|element)',
86 reference : '(string|element)',
91 * ------------------------------------------------------------------------
93 * ------------------------------------------------------------------------
97 constructor(element, config) {
98 this._element = element
100 this._config = this._getConfig(config)
101 this._menu = this._getMenuElement()
102 this._inNavbar = this._detectNavbar()
104 this._addEventListeners()
109 static get VERSION() {
113 static get Default() {
117 static get DefaultType() {
124 if (this._element.disabled || $(this._element).hasClass(ClassName.DISABLED)) {
128 const parent = Dropdown._getParentFromElement(this._element)
129 const isActive = $(this._menu).hasClass(ClassName.SHOW)
131 Dropdown._clearMenus()
137 const relatedTarget = {
138 relatedTarget: this._element
140 const showEvent = $.Event(Event.SHOW, relatedTarget)
142 $(parent).trigger(showEvent)
144 if (showEvent.isDefaultPrevented()) {
148 // Disable totally Popper.js for Dropdown in Navbar
149 if (!this._inNavbar) {
151 * Check for Popper dependency
152 * Popper - https://popper.js.org
154 if (typeof Popper === 'undefined') {
155 throw new TypeError('Bootstrap dropdown require Popper.js (https://popper.js.org)')
158 let referenceElement = this._element
160 if (this._config.reference === 'parent') {
161 referenceElement = parent
162 } else if (Util.isElement(this._config.reference)) {
163 referenceElement = this._config.reference
165 // Check if it's jQuery element
166 if (typeof this._config.reference.jquery !== 'undefined') {
167 referenceElement = this._config.reference[0]
171 // If boundary is not `scrollParent`, then set position to `static`
172 // to allow the menu to "escape" the scroll parent's boundaries
173 // https://github.com/twbs/bootstrap/issues/24251
174 if (this._config.boundary !== 'scrollParent') {
175 $(parent).addClass(ClassName.POSITION_STATIC)
177 this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())
180 // If this is a touch-enabled device we add extra
181 // empty mouseover listeners to the body's immediate children;
182 // only needed because of broken event delegation on iOS
183 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
184 if ('ontouchstart' in document.documentElement &&
185 $(parent).closest(Selector.NAVBAR_NAV).length === 0) {
186 $(document.body).children().on('mouseover', null, $.noop)
189 this._element.focus()
190 this._element.setAttribute('aria-expanded', true)
192 $(this._menu).toggleClass(ClassName.SHOW)
194 .toggleClass(ClassName.SHOW)
195 .trigger($.Event(Event.SHOWN, relatedTarget))
199 $.removeData(this._element, DATA_KEY)
200 $(this._element).off(EVENT_KEY)
203 if (this._popper !== null) {
204 this._popper.destroy()
210 this._inNavbar = this._detectNavbar()
211 if (this._popper !== null) {
212 this._popper.scheduleUpdate()
218 _addEventListeners() {
219 $(this._element).on(Event.CLICK, (event) => {
220 event.preventDefault()
221 event.stopPropagation()
228 ...this.constructor.Default,
229 ...$(this._element).data(),
233 Util.typeCheckConfig(
236 this.constructor.DefaultType
244 const parent = Dropdown._getParentFromElement(this._element)
246 this._menu = parent.querySelector(Selector.MENU)
253 const $parentDropdown = $(this._element.parentNode)
254 let placement = AttachmentMap.BOTTOM
257 if ($parentDropdown.hasClass(ClassName.DROPUP)) {
258 placement = AttachmentMap.TOP
259 if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
260 placement = AttachmentMap.TOPEND
262 } else if ($parentDropdown.hasClass(ClassName.DROPRIGHT)) {
263 placement = AttachmentMap.RIGHT
264 } else if ($parentDropdown.hasClass(ClassName.DROPLEFT)) {
265 placement = AttachmentMap.LEFT
266 } else if ($(this._menu).hasClass(ClassName.MENURIGHT)) {
267 placement = AttachmentMap.BOTTOMEND
273 return $(this._element).closest('.navbar').length > 0
277 const offsetConf = {}
278 if (typeof this._config.offset === 'function') {
279 offsetConf.fn = (data) => {
282 ...this._config.offset(data.offsets) || {}
287 offsetConf.offset = this._config.offset
290 const popperConfig = {
291 placement: this._getPlacement(),
295 enabled: this._config.flip
298 boundariesElement: this._config.boundary
303 // Disable Popper.js if we have a static display
304 if (this._config.display === 'static') {
305 popperConfig.modifiers.applyStyle = {
314 static _jQueryInterface(config) {
315 return this.each(function () {
316 let data = $(this).data(DATA_KEY)
317 const _config = typeof config === 'object' ? config : null
320 data = new Dropdown(this, _config)
321 $(this).data(DATA_KEY, data)
324 if (typeof config === 'string') {
325 if (typeof data[config] === 'undefined') {
326 throw new TypeError(`No method named "${config}"`)
333 static _clearMenus(event) {
334 if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||
335 event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
339 const toggles = [].slice.call(document.querySelectorAll(Selector.DATA_TOGGLE))
340 for (let i = 0, len = toggles.length; i < len; i++) {
341 const parent = Dropdown._getParentFromElement(toggles[i])
342 const context = $(toggles[i]).data(DATA_KEY)
343 const relatedTarget = {
344 relatedTarget: toggles[i]
347 if (event && event.type === 'click') {
348 relatedTarget.clickEvent = event
355 const dropdownMenu = context._menu
356 if (!$(parent).hasClass(ClassName.SHOW)) {
360 if (event && (event.type === 'click' &&
361 /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&
362 $.contains(parent, event.target)) {
366 const hideEvent = $.Event(Event.HIDE, relatedTarget)
367 $(parent).trigger(hideEvent)
368 if (hideEvent.isDefaultPrevented()) {
372 // If this is a touch-enabled device we remove the extra
373 // empty mouseover listeners we added for iOS support
374 if ('ontouchstart' in document.documentElement) {
375 $(document.body).children().off('mouseover', null, $.noop)
378 toggles[i].setAttribute('aria-expanded', 'false')
380 $(dropdownMenu).removeClass(ClassName.SHOW)
382 .removeClass(ClassName.SHOW)
383 .trigger($.Event(Event.HIDDEN, relatedTarget))
387 static _getParentFromElement(element) {
389 const selector = Util.getSelectorFromElement(element)
392 parent = document.querySelector(selector)
395 return parent || element.parentNode
398 // eslint-disable-next-line complexity
399 static _dataApiKeydownHandler(event) {
400 // If not input/textarea:
401 // - And not a key in REGEXP_KEYDOWN => not a dropdown command
402 // If input/textarea:
403 // - If space key => not a dropdown command
404 // - If key is other than escape
405 // - If key is not up or down => not a dropdown command
406 // - If trigger inside the menu => not a dropdown command
407 if (/input|textarea/i.test(event.target.tagName)
408 ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&
409 (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||
410 $(event.target).closest(Selector.MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
414 event.preventDefault()
415 event.stopPropagation()
417 if (this.disabled || $(this).hasClass(ClassName.DISABLED)) {
421 const parent = Dropdown._getParentFromElement(this)
422 const isActive = $(parent).hasClass(ClassName.SHOW)
424 if (!isActive && (event.which !== ESCAPE_KEYCODE || event.which !== SPACE_KEYCODE) ||
425 isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
426 if (event.which === ESCAPE_KEYCODE) {
427 const toggle = parent.querySelector(Selector.DATA_TOGGLE)
428 $(toggle).trigger('focus')
431 $(this).trigger('click')
435 const items = [].slice.call(parent.querySelectorAll(Selector.VISIBLE_ITEMS))
437 if (items.length === 0) {
441 let index = items.indexOf(event.target)
443 if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up
447 if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down
460 * ------------------------------------------------------------------------
461 * Data Api implementation
462 * ------------------------------------------------------------------------
466 .on(Event.KEYDOWN_DATA_API, Selector.DATA_TOGGLE, Dropdown._dataApiKeydownHandler)
467 .on(Event.KEYDOWN_DATA_API, Selector.MENU, Dropdown._dataApiKeydownHandler)
468 .on(`${Event.CLICK_DATA_API} ${Event.KEYUP_DATA_API}`, Dropdown._clearMenus)
469 .on(Event.CLICK_DATA_API, Selector.DATA_TOGGLE, function (event) {
470 event.preventDefault()
471 event.stopPropagation()
472 Dropdown._jQueryInterface.call($(this), 'toggle')
474 .on(Event.CLICK_DATA_API, Selector.FORM_CHILD, (e) => {
479 * ------------------------------------------------------------------------
481 * ------------------------------------------------------------------------
484 $.fn[NAME] = Dropdown._jQueryInterface
485 $.fn[NAME].Constructor = Dropdown
486 $.fn[NAME].noConflict = function () {
487 $.fn[NAME] = JQUERY_NO_CONFLICT
488 return Dropdown._jQueryInterface
494 export default Dropdown