source: imaps-frontend/node_modules/bootstrap/js/src/dropdown.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

  • Property mode set to 100644
File size: 12.9 KB
Line 
1/**
2 * --------------------------------------------------------------------------
3 * Bootstrap dropdown.js
4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
5 * --------------------------------------------------------------------------
6 */
7
8import * as Popper from '@popperjs/core'
9import BaseComponent from './base-component.js'
10import EventHandler from './dom/event-handler.js'
11import Manipulator from './dom/manipulator.js'
12import SelectorEngine from './dom/selector-engine.js'
13import {
14 defineJQueryPlugin,
15 execute,
16 getElement,
17 getNextActiveElement,
18 isDisabled,
19 isElement,
20 isRTL,
21 isVisible,
22 noop
23} from './util/index.js'
24
25/**
26 * Constants
27 */
28
29const NAME = 'dropdown'
30const DATA_KEY = 'bs.dropdown'
31const EVENT_KEY = `.${DATA_KEY}`
32const DATA_API_KEY = '.data-api'
33
34const ESCAPE_KEY = 'Escape'
35const TAB_KEY = 'Tab'
36const ARROW_UP_KEY = 'ArrowUp'
37const ARROW_DOWN_KEY = 'ArrowDown'
38const RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button
39
40const EVENT_HIDE = `hide${EVENT_KEY}`
41const EVENT_HIDDEN = `hidden${EVENT_KEY}`
42const EVENT_SHOW = `show${EVENT_KEY}`
43const EVENT_SHOWN = `shown${EVENT_KEY}`
44const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
45const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
46const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
47
48const CLASS_NAME_SHOW = 'show'
49const CLASS_NAME_DROPUP = 'dropup'
50const CLASS_NAME_DROPEND = 'dropend'
51const CLASS_NAME_DROPSTART = 'dropstart'
52const CLASS_NAME_DROPUP_CENTER = 'dropup-center'
53const CLASS_NAME_DROPDOWN_CENTER = 'dropdown-center'
54
55const SELECTOR_DATA_TOGGLE = '[data-bs-toggle="dropdown"]:not(.disabled):not(:disabled)'
56const SELECTOR_DATA_TOGGLE_SHOWN = `${SELECTOR_DATA_TOGGLE}.${CLASS_NAME_SHOW}`
57const SELECTOR_MENU = '.dropdown-menu'
58const SELECTOR_NAVBAR = '.navbar'
59const SELECTOR_NAVBAR_NAV = '.navbar-nav'
60const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
61
62const PLACEMENT_TOP = isRTL() ? 'top-end' : 'top-start'
63const PLACEMENT_TOPEND = isRTL() ? 'top-start' : 'top-end'
64const PLACEMENT_BOTTOM = isRTL() ? 'bottom-end' : 'bottom-start'
65const PLACEMENT_BOTTOMEND = isRTL() ? 'bottom-start' : 'bottom-end'
66const PLACEMENT_RIGHT = isRTL() ? 'left-start' : 'right-start'
67const PLACEMENT_LEFT = isRTL() ? 'right-start' : 'left-start'
68const PLACEMENT_TOPCENTER = 'top'
69const PLACEMENT_BOTTOMCENTER = 'bottom'
70
71const Default = {
72 autoClose: true,
73 boundary: 'clippingParents',
74 display: 'dynamic',
75 offset: [0, 2],
76 popperConfig: null,
77 reference: 'toggle'
78}
79
80const DefaultType = {
81 autoClose: '(boolean|string)',
82 boundary: '(string|element)',
83 display: 'string',
84 offset: '(array|string|function)',
85 popperConfig: '(null|object|function)',
86 reference: '(string|element|object)'
87}
88
89/**
90 * Class definition
91 */
92
93class Dropdown extends BaseComponent {
94 constructor(element, config) {
95 super(element, config)
96
97 this._popper = null
98 this._parent = this._element.parentNode // dropdown wrapper
99 // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
100 this._menu = SelectorEngine.next(this._element, SELECTOR_MENU)[0] ||
101 SelectorEngine.prev(this._element, SELECTOR_MENU)[0] ||
102 SelectorEngine.findOne(SELECTOR_MENU, this._parent)
103 this._inNavbar = this._detectNavbar()
104 }
105
106 // Getters
107 static get Default() {
108 return Default
109 }
110
111 static get DefaultType() {
112 return DefaultType
113 }
114
115 static get NAME() {
116 return NAME
117 }
118
119 // Public
120 toggle() {
121 return this._isShown() ? this.hide() : this.show()
122 }
123
124 show() {
125 if (isDisabled(this._element) || this._isShown()) {
126 return
127 }
128
129 const relatedTarget = {
130 relatedTarget: this._element
131 }
132
133 const showEvent = EventHandler.trigger(this._element, EVENT_SHOW, relatedTarget)
134
135 if (showEvent.defaultPrevented) {
136 return
137 }
138
139 this._createPopper()
140
141 // If this is a touch-enabled device we add extra
142 // empty mouseover listeners to the body's immediate children;
143 // only needed because of broken event delegation on iOS
144 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
145 if ('ontouchstart' in document.documentElement && !this._parent.closest(SELECTOR_NAVBAR_NAV)) {
146 for (const element of [].concat(...document.body.children)) {
147 EventHandler.on(element, 'mouseover', noop)
148 }
149 }
150
151 this._element.focus()
152 this._element.setAttribute('aria-expanded', true)
153
154 this._menu.classList.add(CLASS_NAME_SHOW)
155 this._element.classList.add(CLASS_NAME_SHOW)
156 EventHandler.trigger(this._element, EVENT_SHOWN, relatedTarget)
157 }
158
159 hide() {
160 if (isDisabled(this._element) || !this._isShown()) {
161 return
162 }
163
164 const relatedTarget = {
165 relatedTarget: this._element
166 }
167
168 this._completeHide(relatedTarget)
169 }
170
171 dispose() {
172 if (this._popper) {
173 this._popper.destroy()
174 }
175
176 super.dispose()
177 }
178
179 update() {
180 this._inNavbar = this._detectNavbar()
181 if (this._popper) {
182 this._popper.update()
183 }
184 }
185
186 // Private
187 _completeHide(relatedTarget) {
188 const hideEvent = EventHandler.trigger(this._element, EVENT_HIDE, relatedTarget)
189 if (hideEvent.defaultPrevented) {
190 return
191 }
192
193 // If this is a touch-enabled device we remove the extra
194 // empty mouseover listeners we added for iOS support
195 if ('ontouchstart' in document.documentElement) {
196 for (const element of [].concat(...document.body.children)) {
197 EventHandler.off(element, 'mouseover', noop)
198 }
199 }
200
201 if (this._popper) {
202 this._popper.destroy()
203 }
204
205 this._menu.classList.remove(CLASS_NAME_SHOW)
206 this._element.classList.remove(CLASS_NAME_SHOW)
207 this._element.setAttribute('aria-expanded', 'false')
208 Manipulator.removeDataAttribute(this._menu, 'popper')
209 EventHandler.trigger(this._element, EVENT_HIDDEN, relatedTarget)
210 }
211
212 _getConfig(config) {
213 config = super._getConfig(config)
214
215 if (typeof config.reference === 'object' && !isElement(config.reference) &&
216 typeof config.reference.getBoundingClientRect !== 'function'
217 ) {
218 // Popper virtual elements require a getBoundingClientRect method
219 throw new TypeError(`${NAME.toUpperCase()}: Option "reference" provided type "object" without a required "getBoundingClientRect" method.`)
220 }
221
222 return config
223 }
224
225 _createPopper() {
226 if (typeof Popper === 'undefined') {
227 throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')
228 }
229
230 let referenceElement = this._element
231
232 if (this._config.reference === 'parent') {
233 referenceElement = this._parent
234 } else if (isElement(this._config.reference)) {
235 referenceElement = getElement(this._config.reference)
236 } else if (typeof this._config.reference === 'object') {
237 referenceElement = this._config.reference
238 }
239
240 const popperConfig = this._getPopperConfig()
241 this._popper = Popper.createPopper(referenceElement, this._menu, popperConfig)
242 }
243
244 _isShown() {
245 return this._menu.classList.contains(CLASS_NAME_SHOW)
246 }
247
248 _getPlacement() {
249 const parentDropdown = this._parent
250
251 if (parentDropdown.classList.contains(CLASS_NAME_DROPEND)) {
252 return PLACEMENT_RIGHT
253 }
254
255 if (parentDropdown.classList.contains(CLASS_NAME_DROPSTART)) {
256 return PLACEMENT_LEFT
257 }
258
259 if (parentDropdown.classList.contains(CLASS_NAME_DROPUP_CENTER)) {
260 return PLACEMENT_TOPCENTER
261 }
262
263 if (parentDropdown.classList.contains(CLASS_NAME_DROPDOWN_CENTER)) {
264 return PLACEMENT_BOTTOMCENTER
265 }
266
267 // We need to trim the value because custom properties can also include spaces
268 const isEnd = getComputedStyle(this._menu).getPropertyValue('--bs-position').trim() === 'end'
269
270 if (parentDropdown.classList.contains(CLASS_NAME_DROPUP)) {
271 return isEnd ? PLACEMENT_TOPEND : PLACEMENT_TOP
272 }
273
274 return isEnd ? PLACEMENT_BOTTOMEND : PLACEMENT_BOTTOM
275 }
276
277 _detectNavbar() {
278 return this._element.closest(SELECTOR_NAVBAR) !== null
279 }
280
281 _getOffset() {
282 const { offset } = this._config
283
284 if (typeof offset === 'string') {
285 return offset.split(',').map(value => Number.parseInt(value, 10))
286 }
287
288 if (typeof offset === 'function') {
289 return popperData => offset(popperData, this._element)
290 }
291
292 return offset
293 }
294
295 _getPopperConfig() {
296 const defaultBsPopperConfig = {
297 placement: this._getPlacement(),
298 modifiers: [{
299 name: 'preventOverflow',
300 options: {
301 boundary: this._config.boundary
302 }
303 },
304 {
305 name: 'offset',
306 options: {
307 offset: this._getOffset()
308 }
309 }]
310 }
311
312 // Disable Popper if we have a static display or Dropdown is in Navbar
313 if (this._inNavbar || this._config.display === 'static') {
314 Manipulator.setDataAttribute(this._menu, 'popper', 'static') // TODO: v6 remove
315 defaultBsPopperConfig.modifiers = [{
316 name: 'applyStyles',
317 enabled: false
318 }]
319 }
320
321 return {
322 ...defaultBsPopperConfig,
323 ...execute(this._config.popperConfig, [defaultBsPopperConfig])
324 }
325 }
326
327 _selectMenuItem({ key, target }) {
328 const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
329
330 if (!items.length) {
331 return
332 }
333
334 // if target isn't included in items (e.g. when expanding the dropdown)
335 // allow cycling to get the last item in case key equals ARROW_UP_KEY
336 getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
337 }
338
339 // Static
340 static jQueryInterface(config) {
341 return this.each(function () {
342 const data = Dropdown.getOrCreateInstance(this, config)
343
344 if (typeof config !== 'string') {
345 return
346 }
347
348 if (typeof data[config] === 'undefined') {
349 throw new TypeError(`No method named "${config}"`)
350 }
351
352 data[config]()
353 })
354 }
355
356 static clearMenus(event) {
357 if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) {
358 return
359 }
360
361 const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN)
362
363 for (const toggle of openToggles) {
364 const context = Dropdown.getInstance(toggle)
365 if (!context || context._config.autoClose === false) {
366 continue
367 }
368
369 const composedPath = event.composedPath()
370 const isMenuTarget = composedPath.includes(context._menu)
371 if (
372 composedPath.includes(context._element) ||
373 (context._config.autoClose === 'inside' && !isMenuTarget) ||
374 (context._config.autoClose === 'outside' && isMenuTarget)
375 ) {
376 continue
377 }
378
379 // Tab navigation through the dropdown menu or events from contained inputs shouldn't close the menu
380 if (context._menu.contains(event.target) && ((event.type === 'keyup' && event.key === TAB_KEY) || /input|select|option|textarea|form/i.test(event.target.tagName))) {
381 continue
382 }
383
384 const relatedTarget = { relatedTarget: context._element }
385
386 if (event.type === 'click') {
387 relatedTarget.clickEvent = event
388 }
389
390 context._completeHide(relatedTarget)
391 }
392 }
393
394 static dataApiKeydownHandler(event) {
395 // If not an UP | DOWN | ESCAPE key => not a dropdown command
396 // If input/textarea && if key is other than ESCAPE => not a dropdown command
397
398 const isInput = /input|textarea/i.test(event.target.tagName)
399 const isEscapeEvent = event.key === ESCAPE_KEY
400 const isUpOrDownEvent = [ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)
401
402 if (!isUpOrDownEvent && !isEscapeEvent) {
403 return
404 }
405
406 if (isInput && !isEscapeEvent) {
407 return
408 }
409
410 event.preventDefault()
411
412 // TODO: v6 revert #37011 & change markup https://getbootstrap.com/docs/5.3/forms/input-group/
413 const getToggleButton = this.matches(SELECTOR_DATA_TOGGLE) ?
414 this :
415 (SelectorEngine.prev(this, SELECTOR_DATA_TOGGLE)[0] ||
416 SelectorEngine.next(this, SELECTOR_DATA_TOGGLE)[0] ||
417 SelectorEngine.findOne(SELECTOR_DATA_TOGGLE, event.delegateTarget.parentNode))
418
419 const instance = Dropdown.getOrCreateInstance(getToggleButton)
420
421 if (isUpOrDownEvent) {
422 event.stopPropagation()
423 instance.show()
424 instance._selectMenuItem(event)
425 return
426 }
427
428 if (instance._isShown()) { // else is escape and we check if it is shown
429 event.stopPropagation()
430 instance.hide()
431 getToggleButton.focus()
432 }
433 }
434}
435
436/**
437 * Data API implementation
438 */
439
440EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown.dataApiKeydownHandler)
441EventHandler.on(document, EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown.dataApiKeydownHandler)
442EventHandler.on(document, EVENT_CLICK_DATA_API, Dropdown.clearMenus)
443EventHandler.on(document, EVENT_KEYUP_DATA_API, Dropdown.clearMenus)
444EventHandler.on(document, EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
445 event.preventDefault()
446 Dropdown.getOrCreateInstance(this).toggle()
447})
448
449/**
450 * jQuery
451 */
452
453defineJQueryPlugin(Dropdown)
454
455export default Dropdown
Note: See TracBrowser for help on using the repository browser.