9
import Popper from 'popper.js'
10
import Util from './util'
16
const NAME = 'dropdown'
17
const VERSION = '4.6.1'
18
const DATA_KEY = 'bs.dropdown'
19
const EVENT_KEY = `.${DATA_KEY}`
20
const DATA_API_KEY = '.data-api'
21
const JQUERY_NO_CONFLICT = $.fn[NAME]
22
const ESCAPE_KEYCODE = 27
23
const SPACE_KEYCODE = 32
25
const ARROW_UP_KEYCODE = 38
26
const ARROW_DOWN_KEYCODE = 40
27
const RIGHT_MOUSE_BUTTON_WHICH = 3
28
const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)
30
const CLASS_NAME_DISABLED = 'disabled'
31
const CLASS_NAME_SHOW = 'show'
32
const CLASS_NAME_DROPUP = 'dropup'
33
const CLASS_NAME_DROPRIGHT = 'dropright'
34
const CLASS_NAME_DROPLEFT = 'dropleft'
35
const CLASS_NAME_MENURIGHT = 'dropdown-menu-right'
36
const CLASS_NAME_POSITION_STATIC = 'position-static'
38
const EVENT_HIDE = `hide${EVENT_KEY}`
39
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
40
const EVENT_SHOW = `show${EVENT_KEY}`
41
const EVENT_SHOWN = `shown${EVENT_KEY}`
42
const EVENT_CLICK = `click${EVENT_KEY}`
43
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
44
const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
45
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
47
const SELECTOR_DATA_TOGGLE = '[data-toggle="dropdown"]'
48
const SELECTOR_FORM_CHILD = '.dropdown form'
49
const SELECTOR_MENU = '.dropdown-menu'
50
const SELECTOR_NAVBAR_NAV = '.navbar-nav'
51
const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
53
const PLACEMENT_TOP = 'top-start'
54
const PLACEMENT_TOPEND = 'top-end'
55
const PLACEMENT_BOTTOM = 'bottom-start'
56
const PLACEMENT_BOTTOMEND = 'bottom-end'
57
const PLACEMENT_RIGHT = 'right-start'
58
const PLACEMENT_LEFT = 'left-start'
63
boundary: 'scrollParent',
70
offset: '(number|string|function)',
72
boundary: '(string|element)',
73
reference: '(string|element)',
75
popperConfig: '(null|object)'
83
constructor(element, config) {
84
this._element = element
86
this._config = this._getConfig(config)
87
this._menu = this._getMenuElement()
88
this._inNavbar = this._detectNavbar()
90
this._addEventListeners()
94
static get VERSION() {
98
static get Default() {
102
static get DefaultType() {
108
if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED)) {
112
const isActive = $(this._menu).hasClass(CLASS_NAME_SHOW)
114
Dropdown._clearMenus()
123
show(usePopper = false) {
124
if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || $(this._menu).hasClass(CLASS_NAME_SHOW)) {
128
const relatedTarget = {
129
relatedTarget: this._element
131
const showEvent = $.Event(EVENT_SHOW, relatedTarget)
132
const parent = Dropdown._getParentFromElement(this._element)
134
$(parent).trigger(showEvent)
136
if (showEvent.isDefaultPrevented()) {
141
if (!this._inNavbar && usePopper) {
143
if (typeof Popper === 'undefined') {
144
throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
147
let referenceElement = this._element
149
if (this._config.reference === 'parent') {
150
referenceElement = parent
151
} else if (Util.isElement(this._config.reference)) {
152
referenceElement = this._config.reference
155
if (typeof this._config.reference.jquery !== 'undefined') {
156
referenceElement = this._config.reference[0]
163
if (this._config.boundary !== 'scrollParent') {
164
$(parent).addClass(CLASS_NAME_POSITION_STATIC)
167
this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())
174
if ('ontouchstart' in document.documentElement &&
175
$(parent).closest(SELECTOR_NAVBAR_NAV).length === 0) {
176
$(document.body).children().on('mouseover', null, $.noop)
179
this._element.focus()
180
this._element.setAttribute('aria-expanded', true)
182
$(this._menu).toggleClass(CLASS_NAME_SHOW)
184
.toggleClass(CLASS_NAME_SHOW)
185
.trigger($.Event(EVENT_SHOWN, relatedTarget))
189
if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || !$(this._menu).hasClass(CLASS_NAME_SHOW)) {
193
const relatedTarget = {
194
relatedTarget: this._element
196
const hideEvent = $.Event(EVENT_HIDE, relatedTarget)
197
const parent = Dropdown._getParentFromElement(this._element)
199
$(parent).trigger(hideEvent)
201
if (hideEvent.isDefaultPrevented()) {
206
this._popper.destroy()
209
$(this._menu).toggleClass(CLASS_NAME_SHOW)
211
.toggleClass(CLASS_NAME_SHOW)
212
.trigger($.Event(EVENT_HIDDEN, relatedTarget))
216
$.removeData(this._element, DATA_KEY)
217
$(this._element).off(EVENT_KEY)
220
if (this._popper !== null) {
221
this._popper.destroy()
227
this._inNavbar = this._detectNavbar()
228
if (this._popper !== null) {
229
this._popper.scheduleUpdate()
234
_addEventListeners() {
235
$(this._element).on(EVENT_CLICK, event => {
236
event.preventDefault()
237
event.stopPropagation()
244
...this.constructor.Default,
245
...$(this._element).data(),
249
Util.typeCheckConfig(
252
this.constructor.DefaultType
260
const parent = Dropdown._getParentFromElement(this._element)
263
this._menu = parent.querySelector(SELECTOR_MENU)
271
const $parentDropdown = $(this._element.parentNode)
272
let placement = PLACEMENT_BOTTOM
275
if ($parentDropdown.hasClass(CLASS_NAME_DROPUP)) {
276
placement = $(this._menu).hasClass(CLASS_NAME_MENURIGHT) ?
279
} else if ($parentDropdown.hasClass(CLASS_NAME_DROPRIGHT)) {
280
placement = PLACEMENT_RIGHT
281
} else if ($parentDropdown.hasClass(CLASS_NAME_DROPLEFT)) {
282
placement = PLACEMENT_LEFT
283
} else if ($(this._menu).hasClass(CLASS_NAME_MENURIGHT)) {
284
placement = PLACEMENT_BOTTOMEND
291
return $(this._element).closest('.navbar').length > 0
297
if (typeof this._config.offset === 'function') {
298
offset.fn = data => {
301
...this._config.offset(data.offsets, this._element)
307
offset.offset = this._config.offset
314
const popperConfig = {
315
placement: this._getPlacement(),
317
offset: this._getOffset(),
319
enabled: this._config.flip
322
boundariesElement: this._config.boundary
328
if (this._config.display === 'static') {
329
popperConfig.modifiers.applyStyle = {
336
...this._config.popperConfig
341
static _jQueryInterface(config) {
342
return this.each(function () {
343
let data = $(this).data(DATA_KEY)
344
const _config = typeof config === 'object' ? config : null
347
data = new Dropdown(this, _config)
348
$(this).data(DATA_KEY, data)
351
if (typeof config === 'string') {
352
if (typeof data[config] === 'undefined') {
353
throw new TypeError(`No method named "${config}"`)
361
static _clearMenus(event) {
362
if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||
363
event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
367
const toggles = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))
369
for (let i = 0, len = toggles.length; i < len; i++) {
370
const parent = Dropdown._getParentFromElement(toggles[i])
371
const context = $(toggles[i]).data(DATA_KEY)
372
const relatedTarget = {
373
relatedTarget: toggles[i]
376
if (event && event.type === 'click') {
377
relatedTarget.clickEvent = event
384
const dropdownMenu = context._menu
385
if (!$(parent).hasClass(CLASS_NAME_SHOW)) {
389
if (event && (event.type === 'click' &&
390
/input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&
391
$.contains(parent, event.target)) {
395
const hideEvent = $.Event(EVENT_HIDE, relatedTarget)
396
$(parent).trigger(hideEvent)
397
if (hideEvent.isDefaultPrevented()) {
403
if ('ontouchstart' in document.documentElement) {
404
$(document.body).children().off('mouseover', null, $.noop)
407
toggles[i].setAttribute('aria-expanded', 'false')
409
if (context._popper) {
410
context._popper.destroy()
413
$(dropdownMenu).removeClass(CLASS_NAME_SHOW)
415
.removeClass(CLASS_NAME_SHOW)
416
.trigger($.Event(EVENT_HIDDEN, relatedTarget))
420
static _getParentFromElement(element) {
422
const selector = Util.getSelectorFromElement(element)
425
parent = document.querySelector(selector)
428
return parent || element.parentNode
432
static _dataApiKeydownHandler(event) {
440
if (/input|textarea/i.test(event.target.tagName) ?
441
event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&
442
(event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||
443
$(event.target).closest(SELECTOR_MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
447
if (this.disabled || $(this).hasClass(CLASS_NAME_DISABLED)) {
451
const parent = Dropdown._getParentFromElement(this)
452
const isActive = $(parent).hasClass(CLASS_NAME_SHOW)
454
if (!isActive && event.which === ESCAPE_KEYCODE) {
458
event.preventDefault()
459
event.stopPropagation()
461
if (!isActive || (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
462
if (event.which === ESCAPE_KEYCODE) {
463
$(parent.querySelector(SELECTOR_DATA_TOGGLE)).trigger('focus')
466
$(this).trigger('click')
470
const items = [].slice.call(parent.querySelectorAll(SELECTOR_VISIBLE_ITEMS))
471
.filter(item => $(item).is(':visible'))
473
if (items.length === 0) {
477
let index = items.indexOf(event.target)
479
if (event.which === ARROW_UP_KEYCODE && index > 0) {
483
if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) {
500
.on(EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown._dataApiKeydownHandler)
501
.on(EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown._dataApiKeydownHandler)
502
.on(`${EVENT_CLICK_DATA_API} ${EVENT_KEYUP_DATA_API}`, Dropdown._clearMenus)
503
.on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
504
event.preventDefault()
505
event.stopPropagation()
506
Dropdown._jQueryInterface.call($(this), 'toggle')
508
.on(EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => {
516
$.fn[NAME] = Dropdown._jQueryInterface
517
$.fn[NAME].Constructor = Dropdown
518
$.fn[NAME].noConflict = () => {
519
$.fn[NAME] = JQUERY_NO_CONFLICT
520
return Dropdown._jQueryInterface
523
export default Dropdown