Layout von Websiten mit Bootstrap und Foundation
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

dropdown.js 15KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. /**
  2. * --------------------------------------------------------------------------
  3. * Bootstrap (v4.5.0): dropdown.js
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
  5. * --------------------------------------------------------------------------
  6. */
  7. import $ from 'jquery'
  8. import Popper from 'popper.js'
  9. import Util from './util'
  10. /**
  11. * ------------------------------------------------------------------------
  12. * Constants
  13. * ------------------------------------------------------------------------
  14. */
  15. const NAME = 'dropdown'
  16. const VERSION = '4.5.0'
  17. const DATA_KEY = 'bs.dropdown'
  18. const EVENT_KEY = `.${DATA_KEY}`
  19. const DATA_API_KEY = '.data-api'
  20. const JQUERY_NO_CONFLICT = $.fn[NAME]
  21. const ESCAPE_KEYCODE = 27 // KeyboardEvent.which value for Escape (Esc) key
  22. const SPACE_KEYCODE = 32 // KeyboardEvent.which value for space key
  23. const TAB_KEYCODE = 9 // KeyboardEvent.which value for tab key
  24. const ARROW_UP_KEYCODE = 38 // KeyboardEvent.which value for up arrow key
  25. const ARROW_DOWN_KEYCODE = 40 // KeyboardEvent.which value for down arrow key
  26. const RIGHT_MOUSE_BUTTON_WHICH = 3 // MouseEvent.which value for the right button (assuming a right-handed mouse)
  27. const REGEXP_KEYDOWN = new RegExp(`${ARROW_UP_KEYCODE}|${ARROW_DOWN_KEYCODE}|${ESCAPE_KEYCODE}`)
  28. const EVENT_HIDE = `hide${EVENT_KEY}`
  29. const EVENT_HIDDEN = `hidden${EVENT_KEY}`
  30. const EVENT_SHOW = `show${EVENT_KEY}`
  31. const EVENT_SHOWN = `shown${EVENT_KEY}`
  32. const EVENT_CLICK = `click${EVENT_KEY}`
  33. const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
  34. const EVENT_KEYDOWN_DATA_API = `keydown${EVENT_KEY}${DATA_API_KEY}`
  35. const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
  36. const CLASS_NAME_DISABLED = 'disabled'
  37. const CLASS_NAME_SHOW = 'show'
  38. const CLASS_NAME_DROPUP = 'dropup'
  39. const CLASS_NAME_DROPRIGHT = 'dropright'
  40. const CLASS_NAME_DROPLEFT = 'dropleft'
  41. const CLASS_NAME_MENURIGHT = 'dropdown-menu-right'
  42. const CLASS_NAME_POSITION_STATIC = 'position-static'
  43. const SELECTOR_DATA_TOGGLE = '[data-toggle="dropdown"]'
  44. const SELECTOR_FORM_CHILD = '.dropdown form'
  45. const SELECTOR_MENU = '.dropdown-menu'
  46. const SELECTOR_NAVBAR_NAV = '.navbar-nav'
  47. const SELECTOR_VISIBLE_ITEMS = '.dropdown-menu .dropdown-item:not(.disabled):not(:disabled)'
  48. const PLACEMENT_TOP = 'top-start'
  49. const PLACEMENT_TOPEND = 'top-end'
  50. const PLACEMENT_BOTTOM = 'bottom-start'
  51. const PLACEMENT_BOTTOMEND = 'bottom-end'
  52. const PLACEMENT_RIGHT = 'right-start'
  53. const PLACEMENT_LEFT = 'left-start'
  54. const Default = {
  55. offset : 0,
  56. flip : true,
  57. boundary : 'scrollParent',
  58. reference : 'toggle',
  59. display : 'dynamic',
  60. popperConfig : null
  61. }
  62. const DefaultType = {
  63. offset : '(number|string|function)',
  64. flip : 'boolean',
  65. boundary : '(string|element)',
  66. reference : '(string|element)',
  67. display : 'string',
  68. popperConfig : '(null|object)'
  69. }
  70. /**
  71. * ------------------------------------------------------------------------
  72. * Class Definition
  73. * ------------------------------------------------------------------------
  74. */
  75. class Dropdown {
  76. constructor(element, config) {
  77. this._element = element
  78. this._popper = null
  79. this._config = this._getConfig(config)
  80. this._menu = this._getMenuElement()
  81. this._inNavbar = this._detectNavbar()
  82. this._addEventListeners()
  83. }
  84. // Getters
  85. static get VERSION() {
  86. return VERSION
  87. }
  88. static get Default() {
  89. return Default
  90. }
  91. static get DefaultType() {
  92. return DefaultType
  93. }
  94. // Public
  95. toggle() {
  96. if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED)) {
  97. return
  98. }
  99. const isActive = $(this._menu).hasClass(CLASS_NAME_SHOW)
  100. Dropdown._clearMenus()
  101. if (isActive) {
  102. return
  103. }
  104. this.show(true)
  105. }
  106. show(usePopper = false) {
  107. if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || $(this._menu).hasClass(CLASS_NAME_SHOW)) {
  108. return
  109. }
  110. const relatedTarget = {
  111. relatedTarget: this._element
  112. }
  113. const showEvent = $.Event(EVENT_SHOW, relatedTarget)
  114. const parent = Dropdown._getParentFromElement(this._element)
  115. $(parent).trigger(showEvent)
  116. if (showEvent.isDefaultPrevented()) {
  117. return
  118. }
  119. // Disable totally Popper.js for Dropdown in Navbar
  120. if (!this._inNavbar && usePopper) {
  121. /**
  122. * Check for Popper dependency
  123. * Popper - https://popper.js.org
  124. */
  125. if (typeof Popper === 'undefined') {
  126. throw new TypeError('Bootstrap\'s dropdowns require Popper.js (https://popper.js.org/)')
  127. }
  128. let referenceElement = this._element
  129. if (this._config.reference === 'parent') {
  130. referenceElement = parent
  131. } else if (Util.isElement(this._config.reference)) {
  132. referenceElement = this._config.reference
  133. // Check if it's jQuery element
  134. if (typeof this._config.reference.jquery !== 'undefined') {
  135. referenceElement = this._config.reference[0]
  136. }
  137. }
  138. // If boundary is not `scrollParent`, then set position to `static`
  139. // to allow the menu to "escape" the scroll parent's boundaries
  140. // https://github.com/twbs/bootstrap/issues/24251
  141. if (this._config.boundary !== 'scrollParent') {
  142. $(parent).addClass(CLASS_NAME_POSITION_STATIC)
  143. }
  144. this._popper = new Popper(referenceElement, this._menu, this._getPopperConfig())
  145. }
  146. // If this is a touch-enabled device we add extra
  147. // empty mouseover listeners to the body's immediate children;
  148. // only needed because of broken event delegation on iOS
  149. // https://www.quirksmode.org/blog/archives/2014/02/mouse_event_bub.html
  150. if ('ontouchstart' in document.documentElement &&
  151. $(parent).closest(SELECTOR_NAVBAR_NAV).length === 0) {
  152. $(document.body).children().on('mouseover', null, $.noop)
  153. }
  154. this._element.focus()
  155. this._element.setAttribute('aria-expanded', true)
  156. $(this._menu).toggleClass(CLASS_NAME_SHOW)
  157. $(parent)
  158. .toggleClass(CLASS_NAME_SHOW)
  159. .trigger($.Event(EVENT_SHOWN, relatedTarget))
  160. }
  161. hide() {
  162. if (this._element.disabled || $(this._element).hasClass(CLASS_NAME_DISABLED) || !$(this._menu).hasClass(CLASS_NAME_SHOW)) {
  163. return
  164. }
  165. const relatedTarget = {
  166. relatedTarget: this._element
  167. }
  168. const hideEvent = $.Event(EVENT_HIDE, relatedTarget)
  169. const parent = Dropdown._getParentFromElement(this._element)
  170. $(parent).trigger(hideEvent)
  171. if (hideEvent.isDefaultPrevented()) {
  172. return
  173. }
  174. if (this._popper) {
  175. this._popper.destroy()
  176. }
  177. $(this._menu).toggleClass(CLASS_NAME_SHOW)
  178. $(parent)
  179. .toggleClass(CLASS_NAME_SHOW)
  180. .trigger($.Event(EVENT_HIDDEN, relatedTarget))
  181. }
  182. dispose() {
  183. $.removeData(this._element, DATA_KEY)
  184. $(this._element).off(EVENT_KEY)
  185. this._element = null
  186. this._menu = null
  187. if (this._popper !== null) {
  188. this._popper.destroy()
  189. this._popper = null
  190. }
  191. }
  192. update() {
  193. this._inNavbar = this._detectNavbar()
  194. if (this._popper !== null) {
  195. this._popper.scheduleUpdate()
  196. }
  197. }
  198. // Private
  199. _addEventListeners() {
  200. $(this._element).on(EVENT_CLICK, (event) => {
  201. event.preventDefault()
  202. event.stopPropagation()
  203. this.toggle()
  204. })
  205. }
  206. _getConfig(config) {
  207. config = {
  208. ...this.constructor.Default,
  209. ...$(this._element).data(),
  210. ...config
  211. }
  212. Util.typeCheckConfig(
  213. NAME,
  214. config,
  215. this.constructor.DefaultType
  216. )
  217. return config
  218. }
  219. _getMenuElement() {
  220. if (!this._menu) {
  221. const parent = Dropdown._getParentFromElement(this._element)
  222. if (parent) {
  223. this._menu = parent.querySelector(SELECTOR_MENU)
  224. }
  225. }
  226. return this._menu
  227. }
  228. _getPlacement() {
  229. const $parentDropdown = $(this._element.parentNode)
  230. let placement = PLACEMENT_BOTTOM
  231. // Handle dropup
  232. if ($parentDropdown.hasClass(CLASS_NAME_DROPUP)) {
  233. placement = $(this._menu).hasClass(CLASS_NAME_MENURIGHT)
  234. ? PLACEMENT_TOPEND
  235. : PLACEMENT_TOP
  236. } else if ($parentDropdown.hasClass(CLASS_NAME_DROPRIGHT)) {
  237. placement = PLACEMENT_RIGHT
  238. } else if ($parentDropdown.hasClass(CLASS_NAME_DROPLEFT)) {
  239. placement = PLACEMENT_LEFT
  240. } else if ($(this._menu).hasClass(CLASS_NAME_MENURIGHT)) {
  241. placement = PLACEMENT_BOTTOMEND
  242. }
  243. return placement
  244. }
  245. _detectNavbar() {
  246. return $(this._element).closest('.navbar').length > 0
  247. }
  248. _getOffset() {
  249. const offset = {}
  250. if (typeof this._config.offset === 'function') {
  251. offset.fn = (data) => {
  252. data.offsets = {
  253. ...data.offsets,
  254. ...this._config.offset(data.offsets, this._element) || {}
  255. }
  256. return data
  257. }
  258. } else {
  259. offset.offset = this._config.offset
  260. }
  261. return offset
  262. }
  263. _getPopperConfig() {
  264. const popperConfig = {
  265. placement: this._getPlacement(),
  266. modifiers: {
  267. offset: this._getOffset(),
  268. flip: {
  269. enabled: this._config.flip
  270. },
  271. preventOverflow: {
  272. boundariesElement: this._config.boundary
  273. }
  274. }
  275. }
  276. // Disable Popper.js if we have a static display
  277. if (this._config.display === 'static') {
  278. popperConfig.modifiers.applyStyle = {
  279. enabled: false
  280. }
  281. }
  282. return {
  283. ...popperConfig,
  284. ...this._config.popperConfig
  285. }
  286. }
  287. // Static
  288. static _jQueryInterface(config) {
  289. return this.each(function () {
  290. let data = $(this).data(DATA_KEY)
  291. const _config = typeof config === 'object' ? config : null
  292. if (!data) {
  293. data = new Dropdown(this, _config)
  294. $(this).data(DATA_KEY, data)
  295. }
  296. if (typeof config === 'string') {
  297. if (typeof data[config] === 'undefined') {
  298. throw new TypeError(`No method named "${config}"`)
  299. }
  300. data[config]()
  301. }
  302. })
  303. }
  304. static _clearMenus(event) {
  305. if (event && (event.which === RIGHT_MOUSE_BUTTON_WHICH ||
  306. event.type === 'keyup' && event.which !== TAB_KEYCODE)) {
  307. return
  308. }
  309. const toggles = [].slice.call(document.querySelectorAll(SELECTOR_DATA_TOGGLE))
  310. for (let i = 0, len = toggles.length; i < len; i++) {
  311. const parent = Dropdown._getParentFromElement(toggles[i])
  312. const context = $(toggles[i]).data(DATA_KEY)
  313. const relatedTarget = {
  314. relatedTarget: toggles[i]
  315. }
  316. if (event && event.type === 'click') {
  317. relatedTarget.clickEvent = event
  318. }
  319. if (!context) {
  320. continue
  321. }
  322. const dropdownMenu = context._menu
  323. if (!$(parent).hasClass(CLASS_NAME_SHOW)) {
  324. continue
  325. }
  326. if (event && (event.type === 'click' &&
  327. /input|textarea/i.test(event.target.tagName) || event.type === 'keyup' && event.which === TAB_KEYCODE) &&
  328. $.contains(parent, event.target)) {
  329. continue
  330. }
  331. const hideEvent = $.Event(EVENT_HIDE, relatedTarget)
  332. $(parent).trigger(hideEvent)
  333. if (hideEvent.isDefaultPrevented()) {
  334. continue
  335. }
  336. // If this is a touch-enabled device we remove the extra
  337. // empty mouseover listeners we added for iOS support
  338. if ('ontouchstart' in document.documentElement) {
  339. $(document.body).children().off('mouseover', null, $.noop)
  340. }
  341. toggles[i].setAttribute('aria-expanded', 'false')
  342. if (context._popper) {
  343. context._popper.destroy()
  344. }
  345. $(dropdownMenu).removeClass(CLASS_NAME_SHOW)
  346. $(parent)
  347. .removeClass(CLASS_NAME_SHOW)
  348. .trigger($.Event(EVENT_HIDDEN, relatedTarget))
  349. }
  350. }
  351. static _getParentFromElement(element) {
  352. let parent
  353. const selector = Util.getSelectorFromElement(element)
  354. if (selector) {
  355. parent = document.querySelector(selector)
  356. }
  357. return parent || element.parentNode
  358. }
  359. // eslint-disable-next-line complexity
  360. static _dataApiKeydownHandler(event) {
  361. // If not input/textarea:
  362. // - And not a key in REGEXP_KEYDOWN => not a dropdown command
  363. // If input/textarea:
  364. // - If space key => not a dropdown command
  365. // - If key is other than escape
  366. // - If key is not up or down => not a dropdown command
  367. // - If trigger inside the menu => not a dropdown command
  368. if (/input|textarea/i.test(event.target.tagName)
  369. ? event.which === SPACE_KEYCODE || event.which !== ESCAPE_KEYCODE &&
  370. (event.which !== ARROW_DOWN_KEYCODE && event.which !== ARROW_UP_KEYCODE ||
  371. $(event.target).closest(SELECTOR_MENU).length) : !REGEXP_KEYDOWN.test(event.which)) {
  372. return
  373. }
  374. if (this.disabled || $(this).hasClass(CLASS_NAME_DISABLED)) {
  375. return
  376. }
  377. const parent = Dropdown._getParentFromElement(this)
  378. const isActive = $(parent).hasClass(CLASS_NAME_SHOW)
  379. if (!isActive && event.which === ESCAPE_KEYCODE) {
  380. return
  381. }
  382. event.preventDefault()
  383. event.stopPropagation()
  384. if (!isActive || isActive && (event.which === ESCAPE_KEYCODE || event.which === SPACE_KEYCODE)) {
  385. if (event.which === ESCAPE_KEYCODE) {
  386. $(parent.querySelector(SELECTOR_DATA_TOGGLE)).trigger('focus')
  387. }
  388. $(this).trigger('click')
  389. return
  390. }
  391. const items = [].slice.call(parent.querySelectorAll(SELECTOR_VISIBLE_ITEMS))
  392. .filter((item) => $(item).is(':visible'))
  393. if (items.length === 0) {
  394. return
  395. }
  396. let index = items.indexOf(event.target)
  397. if (event.which === ARROW_UP_KEYCODE && index > 0) { // Up
  398. index--
  399. }
  400. if (event.which === ARROW_DOWN_KEYCODE && index < items.length - 1) { // Down
  401. index++
  402. }
  403. if (index < 0) {
  404. index = 0
  405. }
  406. items[index].focus()
  407. }
  408. }
  409. /**
  410. * ------------------------------------------------------------------------
  411. * Data Api implementation
  412. * ------------------------------------------------------------------------
  413. */
  414. $(document)
  415. .on(EVENT_KEYDOWN_DATA_API, SELECTOR_DATA_TOGGLE, Dropdown._dataApiKeydownHandler)
  416. .on(EVENT_KEYDOWN_DATA_API, SELECTOR_MENU, Dropdown._dataApiKeydownHandler)
  417. .on(`${EVENT_CLICK_DATA_API} ${EVENT_KEYUP_DATA_API}`, Dropdown._clearMenus)
  418. .on(EVENT_CLICK_DATA_API, SELECTOR_DATA_TOGGLE, function (event) {
  419. event.preventDefault()
  420. event.stopPropagation()
  421. Dropdown._jQueryInterface.call($(this), 'toggle')
  422. })
  423. .on(EVENT_CLICK_DATA_API, SELECTOR_FORM_CHILD, (e) => {
  424. e.stopPropagation()
  425. })
  426. /**
  427. * ------------------------------------------------------------------------
  428. * jQuery
  429. * ------------------------------------------------------------------------
  430. */
  431. $.fn[NAME] = Dropdown._jQueryInterface
  432. $.fn[NAME].Constructor = Dropdown
  433. $.fn[NAME].noConflict = () => {
  434. $.fn[NAME] = JQUERY_NO_CONFLICT
  435. return Dropdown._jQueryInterface
  436. }
  437. export default Dropdown