codekingpro/portable-devtools
115k
1/**2 * --------------------------------------------------------------------------3 * Bootstrap (v4.6.2): dropdown.js4 * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)5 * --------------------------------------------------------------------------6 */7 8import $ from 'jquery'9import Popper from 'popper.js'10import Util from './util'11 12/**13 * Constants14 */15 16const NAME = 'dropdown'17const VERSION = '4.6.2'18const DATA_KEY = 'bs.dropdown'19const EVENT_KEY = `.${DATA_KEY}`20const DATA_API_KEY = '.data-api'21const JQUERY_NO_CONFLICT = $.fn[NAME]22const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key23const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key24const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key25const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key26const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key27const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)28const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)29 30const CLASS_NAME_DISABLED = 'disabled'31const CLASS_NAME_SHOW = 'show'32const CLASS_NAME_DROPUP = 'dropup'33const CLASS_NAME_DROPRIGHT = 'dropright'34const CLASS_NAME_DROPLEFT = 'dropleft'35const CLASS_NAME_MENURIGHT = 'dropdown-menu-right'36const CLASS_NAME_POSITION_STATIC = 'position-static'37 38const EVENT_HIDE = `hide${EVENT_KEY}`39const EVENT_HIDDEN = `hidden${EVENT_KEY}`40const EVENT_SHOW = `show${EVENT_KEY}`41const EVENT_SHOWN = `shown${EVENT_KEY}`42const EVENT_CLICK = `click${EVENT_KEY}`43const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`44const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`45const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`46 47const SELECTOR_DATA_TOGGLE = '[data-toggle="dropdown"]'48const SELECTOR_FORM_CHILD = '.dropdown form'49const SELECTOR_MENU = '.dropdown-menu'50const SELECTOR_NAVBAR_NAV = '.navbar-nav'51const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'52 53const PLACEMENT_TOP = 'top-start'54const PLACEMENT_TOPEND = 'top-end'55const PLACEMENT_BOTTOM = 'bottom-start'56const PLACEMENT_BOTTOMEND = 'bottom-end'57const PLACEMENT_RIGHT = 'right-start'58const PLACEMENT_LEFT = 'left-start'59 60const Default = {61 offset: 0,62 flip: true,63 boundary: 'scrollParent',64 reference: 'toggle',65 display: 'dynamic',66 popperConfig: null67}68 69const DefaultType = {70 offset: '(number|string|function)',71 flip: 'boolean',72 boundary: '(string|element)',73 reference: '(string|element)',74 display: 'string',75 popperConfig: '(null|object)'76}77 78/**79 * Class definition80 */81 82class Dropdown {83 constructor(element, config) {84 this._element = element85 this._popper = null86 this._config = this._getConfig(config)87 this._menu = this._getMenuElement()88 this._inNavbar = this._detectNavbar()89 90 this._addEventListeners()91 }92 93 // Getters94 static get VERSION() {95 return VERSION96 }97 98 static get Default() {99 return Default100 }101 102 static get DefaultType() {103 return DefaultType104 }105 106 // Public107 toggle() {108 if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED)) {109 return110 }111 112 const isActive = $(this._menu).hasClass(CLASS_NAME_SHOW)113 114 Dropdown._clearMenus()115 116 if (isActive) {117 return118 }119 120 this.show(true)121 }122 123 show(usePopper = false) {124 if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || $(this._menu).hasClass(CLASS_NAME_SHOW)) {125 return126 }127 128 const relatedTarget = {129 relatedTarget: this._element130 }131 const showEvent = $.Event(EVENT_SHOW, relatedTarget)132 const parent = Dropdown._getParentFromElement(this._element)133 134 $(parent).trigger(showEvent)135 136 if (showEvent.isDefaultPrevented()) {137 return138 }139 140 // Totally disable Popper for Dropdowns in Navbar141 if (!this._inNavbar && usePopper) {142 // Check for Popper dependency143 if (typeof Popper === 'undefined') {144 throw new TypeError('Bootstrap\'s dropdowns require Popper (https://popper.js.org)')145 }146 147 let referenceElement = this._element148 149 if (this._config.reference === 'parent') {150 referenceElement = parent151 } else if (Util.isElement(this._config.reference)) {152 referenceElement = this._config.reference153 154 // Check if it's jQuery element155 if (typeof this._config.reference.jquery !== 'undefined') {156 referenceElement = this._config.reference[0]157 }158 }159 160 // If boundary is not `scrollParent`, then set position to `static`161 // to allow the menu to "escape" the scroll parent's boundaries162 // https://github.com/twbs/bootstrap/issues/24251163 if (this._config.boundary !== 'scrollParent') {164 $(parent).addClass(CLASS_NAME_POSITION_STATIC)165 }166 167 this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())168 }169 170 // If this is a touch-enabled device we add extra171 // empty mouseover listeners to the body's immediate children;172 // only needed because of broken event delegation on iOS173 // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html174 if ('ontouchstart' in document.documentElement &&175 $(parent).closest(SELECTOR_NAVBAR_NAV).length === 0) {176 $(document.body).children().on('mouseover', null, $.noop)177 }178 179 this._element.focus()180 this._element.setAttribute('aria-expanded', true)181 182 $(this._menu).toggleClass(CLASS_NAME_SHOW)183 $(parent)184 .toggleClass(CLASS_NAME_SHOW)185 .trigger($.Event(EVENT_SHOWN, relatedTarget))186 }187 188 hide() {189 if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || !$(this._menu).hasClass(CLASS_NAME_SHOW)) {190 return191 }192 193 const relatedTarget = {194 relatedTarget: this._element195 }196 const hideEvent = $.Event(EVENT_HIDE, relatedTarget)197 const parent = Dropdown._getParentFromElement(this._element)198 199 $(parent).trigger(hideEvent)200 201 if (hideEvent.isDefaultPrevented()) {202 return203 }204 205 if (this._popper) {206 this._popper.destroy()207 }208 209 $(this._menu).toggleClass(CLASS_NAME_SHOW)210 $(parent)211 .toggleClass(CLASS_NAME_SHOW)212 .trigger($.Event(EVENT_HIDDEN, relatedTarget))213 }214 215 dispose() {216 $.removeData(this._element, DATA_KEY)217 $(this._element).off(EVENT_KEY)218 this._element = null219 this._menu = null220 if (this._popper !== null) {221 this._popper.destroy()222 this._popper = null223 }224 }225 226 update() {227 this._inNavbar = this._detectNavbar()228 if (this._popper !== null) {229 this._popper.scheduleUpdate()230 }231 }232 233 // Private234 _addEventListeners() {235 $(this._element).on(EVENT_CLICK, event => {236 event.preventDefault()237 event.stopPropagation()238 this.toggle()239 })240 }241 242 _getConfig(config) {243 config = {244 ...this.constructor.Default,245 ...$(this._element).data(),246 ...config247 }248 249 Util.typeCheckConfig(250 NAME,251 config,252 this.constructor.DefaultType253 )254 255 return config256 }257 258 _getMenuElement() {259 if (!this._menu) {260 const parent = Dropdown._getParentFromElement(this._element)261 262 if (parent) {263 this._menu = parent.querySelector(SELECTOR_MENU)264 }265 }266 267 return this._menu268 }269 270 _getPlacement() {271 const $parentDropdown = $(this._element.parentNode)272 let placement = PLACEMENT_BOTTOM273 274 // Handle dropup275 if ($parentDropdown.hasClass(CLASS_NAME_DROPUP)) {276 placement = $(this._menu).hasClass(CLASS_NAME_MENURIGHT) ?277 PLACEMENT_TOPEND :278 PLACEMENT_TOP279 } else if ($parentDropdown.hasClass(CLASS_NAME_DROPRIGHT)) {280 placement = PLACEMENT_RIGHT281 } else if ($parentDropdown.hasClass(CLASS_NAME_DROPLEFT)) {282 placement = PLACEMENT_LEFT283 } else if ($(this._menu).hasClass(CLASS_NAME_MENURIGHT)) {284 placement = PLACEMENT_BOTTOMEND285 }286 287 return placement288 }289 290 _detectNavbar() {291 return $(this._element).closest('.navbar').length > 0292 }293 294 _getOffset() {295 const offset = {}296 297 if (typeof this._config.offset === 'function') {298 offset.fn = data => {299 data.offsets = {300 ...data.offsets,301 ...this._config.offset(data.offsets, this._element)302 }303 304 return data305 }306 } else {307 offset.offset = this._config.offset308 }309 310 return offset311 }312 313 _getPopperConfig() {314 const popperConfig = {315 placement: this._getPlacement(),316 modifiers: {317 offset: this._getOffset(),318 flip: {319 enabled: this._config.flip320 },321 preventOverflow: {322 boundariesElement: this._config.boundary323 }324 }325 }326 327 // Disable Popper if we have a static display328 if (this._config.display === 'static') {329 popperConfig.modifiers.applyStyle = {330 enabled: false331 }332 }333 334 return {335 ...popperConfig,336 ...this._config.popperConfig337 }338 }339 340 // Static341 static _jQueryInterface(config) {342 return this.each(function () {343 let data = $(this).data(DATA_KEY)344 const _config = typeof config === 'object' ? config : null345 346 if (!data) {347 data = new Dropdown(this, _config)348 $(this).data(DATA_KEY, data)349 }350 351 if (typeof config === 'string') {352 if (typeof data[config] === 'undefined') {353 throw new TypeError(`No method named "${config}"`)354 }355 356 data[config]()357 }358 })359 }360 361 static _clearMenus(event) {362 if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||363 event.type === 'keyup' && event.which !== TAB_KEYCODE)) {364 return365 }366 367 const toggles = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))368 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]374 }375 376 if (event && event.type === 'click') {377 relatedTarget.clickEvent = event378 }379 380 if (!context) {381 continue382 }383 384 const dropdownMenu = context._menu385 if (!$(parent).hasClass(CLASS_NAME_SHOW)) {386 continue387 }388 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)) {392 continue393 }394 395 const hideEvent = $.Event(EVENT_HIDE, relatedTarget)396 $(parent).trigger(hideEvent)397 if (hideEvent.isDefaultPrevented()) {398 continue399 }400 401 // If this is a touch-enabled device we remove the extra402 // empty mouseover listeners we added for iOS support403 if ('ontouchstart' in document.documentElement) {404 $(document.body).children().off('mouseover', null, $.noop)405 }406 407 toggles[i].setAttribute('aria-expanded', 'false')408 409 if (context._popper) {410 context._popper.destroy()411 }412 413 $(dropdownMenu).removeClass(CLASS_NAME_SHOW)414 $(parent)415 .removeClass(CLASS_NAME_SHOW)416 .trigger($.Event(EVENT_HIDDEN, relatedTarget))417 }418 }419 420 static _getParentFromElement(element) {421 let parent422 const selector = Util.getSelectorFromElement(element)423 424 if (selector) {425 parent = document.querySelector(selector)426 }427 428 return parent || element.parentNode429 }430 431 // eslint-disable-next-line complexity432 static _dataApiKeydownHandler(event) {433 // If not input/textarea:434 // - And not a key in REGEXP_KEYDOWN => not a dropdown command435 // If input/textarea:436 // - If space key => not a dropdown command437 // - If key is other than escape438 // - If key is not up or down => not a dropdown command439 // - If trigger inside the menu => not a dropdown command440 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)) {444 return445 }446 447 if (this.disabled || $(this).hasClass(CLASS_NAME_DISABLED)) {448 return449 }450 451 const parent = Dropdown._getParentFromElement(this)452 const isActive = $(parent).hasClass(CLASS_NAME_SHOW)453 454 if (!isActive && event.which === ESCAPE_KEYCODE) {455 return456 }457 458 event.preventDefault()459 event.stopPropagation()460 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')464 }465 466 $(this).trigger('click')467 return468 }469 470 const items = [].slice.call(parent.querySelectorAll(SELECTOR_VISIBLE_ITEMS))471 .filter(item => $(item).is(':visible'))472 473 if (items.length === 0) {474 return475 }476 477 let index = items.indexOf(event.target)478 479 if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up480 index--481 }482 483 if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down484 index++485 }486 487 if (index < 0) {488 index = 0489 }490 491 items[index].focus()492 }493}494 495/**496 * Data API implementation497 */498 499$(document)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')507 })508 .on(EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, e => {509 e.stopPropagation()510 })511 512/**513 * jQuery514 */515 516$.fn[NAME] = Dropdown._jQueryInterface517$.fn[NAME].Constructor = Dropdown518$.fn[NAME].noConflict = () => {519 $.fn[NAME] = JQUERY_NO_CONFLICT520 return Dropdown._jQueryInterface521}522 523export default Dropdown524 