theme/boost/amd/src/bootstrap/dom/event-handler.js

  1. /**
  2. * --------------------------------------------------------------------------
  3. * Bootstrap dom/event-handler.js
  4. * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE)
  5. * --------------------------------------------------------------------------
  6. */
  7. import { getjQuery } from '../util/index'
  8. /**
  9. * Constants
  10. */
  11. const namespaceRegex = /[^.]*(?=\..*)\.|.*/
  12. const stripNameRegex = /\..*/
  13. const stripUidRegex = /::\d+$/
  14. const eventRegistry = {} // Events storage
  15. let uidEvent = 1
  16. const customEvents = {
  17. mouseenter: 'mouseover',
  18. mouseleave: 'mouseout'
  19. }
  20. const nativeEvents = new Set([
  21. 'click',
  22. 'dblclick',
  23. 'mouseup',
  24. 'mousedown',
  25. 'contextmenu',
  26. 'mousewheel',
  27. 'DOMMouseScroll',
  28. 'mouseover',
  29. 'mouseout',
  30. 'mousemove',
  31. 'selectstart',
  32. 'selectend',
  33. 'keydown',
  34. 'keypress',
  35. 'keyup',
  36. 'orientationchange',
  37. 'touchstart',
  38. 'touchmove',
  39. 'touchend',
  40. 'touchcancel',
  41. 'pointerdown',
  42. 'pointermove',
  43. 'pointerup',
  44. 'pointerleave',
  45. 'pointercancel',
  46. 'gesturestart',
  47. 'gesturechange',
  48. 'gestureend',
  49. 'focus',
  50. 'blur',
  51. 'change',
  52. 'reset',
  53. 'select',
  54. 'submit',
  55. 'focusin',
  56. 'focusout',
  57. 'load',
  58. 'unload',
  59. 'beforeunload',
  60. 'resize',
  61. 'move',
  62. 'DOMContentLoaded',
  63. 'readystatechange',
  64. 'error',
  65. 'abort',
  66. 'scroll'
  67. ])
  68. /**
  69. * Private methods
  70. */
  71. function makeEventUid(element, uid) {
  72. return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
  73. }
  74. function getElementEvents(element) {
  75. const uid = makeEventUid(element)
  76. element.uidEvent = uid
  77. eventRegistry[uid] = eventRegistry[uid] || {}
  78. return eventRegistry[uid]
  79. }
  80. function bootstrapHandler(element, fn) {
  81. return function handler(event) {
  82. hydrateObj(event, { delegateTarget: element })
  83. if (handler.oneOff) {
  84. EventHandler.off(element, event.type, fn)
  85. }
  86. return fn.apply(element, [event])
  87. }
  88. }
  89. function bootstrapDelegationHandler(element, selector, fn) {
  90. return function handler(event) {
  91. const domElements = element.querySelectorAll(selector)
  92. for (let { target } = event; target && target !== this; target = target.parentNode) {
  93. for (const domElement of domElements) {
  94. if (domElement !== target) {
  95. continue
  96. }
  97. hydrateObj(event, { delegateTarget: target })
  98. if (handler.oneOff) {
  99. EventHandler.off(element, event.type, selector, fn)
  100. }
  101. return fn.apply(target, [event])
  102. }
  103. }
  104. }
  105. }
  106. function findHandler(events, callable, delegationSelector = null) {
  107. return Object.values(events)
  108. .find(event => event.callable === callable && event.delegationSelector === delegationSelector)
  109. }
  110. function normalizeParameters(originalTypeEvent, handler, delegationFunction) {
  111. const isDelegated = typeof handler === 'string'
  112. // TODO: tooltip passes `false` instead of selector, so we need to check
  113. const callable = isDelegated ? delegationFunction : (handler || delegationFunction)
  114. let typeEvent = getTypeEvent(originalTypeEvent)
  115. if (!nativeEvents.has(typeEvent)) {
  116. typeEvent = originalTypeEvent
  117. }
  118. return [isDelegated, callable, typeEvent]
  119. }
  120. function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {
  121. if (typeof originalTypeEvent !== 'string' || !element) {
  122. return
  123. }
  124. let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
  125. // in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
  126. // this prevents the handler from being dispatched the same way as mouseover or mouseout does
  127. if (originalTypeEvent in customEvents) {
  128. const wrapFunction = fn => {
  129. return function (event) {
  130. if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) {
  131. return fn.call(this, event)
  132. }
  133. }
  134. }
  135. callable = wrapFunction(callable)
  136. }
  137. const events = getElementEvents(element)
  138. const handlers = events[typeEvent] || (events[typeEvent] = {})
  139. const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)
  140. if (previousFunction) {
  141. previousFunction.oneOff = previousFunction.oneOff && oneOff
  142. return
  143. }
  144. const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))
  145. const fn = isDelegated ?
  146. bootstrapDelegationHandler(element, handler, callable) :
  147. bootstrapHandler(element, callable)
  148. fn.delegationSelector = isDelegated ? handler : null
  149. fn.callable = callable
  150. fn.oneOff = oneOff
  151. fn.uidEvent = uid
  152. handlers[uid] = fn
  153. element.addEventListener(typeEvent, fn, isDelegated)
  154. }
  155. function removeHandler(element, events, typeEvent, handler, delegationSelector) {
  156. const fn = findHandler(events[typeEvent], handler, delegationSelector)
  157. if (!fn) {
  158. return
  159. }
  160. element.removeEventListener(typeEvent, fn, Boolean(delegationSelector))
  161. delete events[typeEvent][fn.uidEvent]
  162. }
  163. function removeNamespacedHandlers(element, events, typeEvent, namespace) {
  164. const storeElementEvent = events[typeEvent] || {}
  165. for (const [handlerKey, event] of Object.entries(storeElementEvent)) {
  166. if (handlerKey.includes(namespace)) {
  167. removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
  168. }
  169. }
  170. }
  171. function getTypeEvent(event) {
  172. // allow to get the native events from namespaced events ('click.bs.button' --> 'click')
  173. event = event.replace(stripNameRegex, '')
  174. return customEvents[event] || event
  175. }
  176. const EventHandler = {
  177. on(element, event, handler, delegationFunction) {
  178. addHandler(element, event, handler, delegationFunction, false)
  179. },
  180. one(element, event, handler, delegationFunction) {
  181. addHandler(element, event, handler, delegationFunction, true)
  182. },
  183. off(element, originalTypeEvent, handler, delegationFunction) {
  184. if (typeof originalTypeEvent !== 'string' || !element) {
  185. return
  186. }
  187. const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
  188. const inNamespace = typeEvent !== originalTypeEvent
  189. const events = getElementEvents(element)
  190. const storeElementEvent = events[typeEvent] || {}
  191. const isNamespace = originalTypeEvent.startsWith('.')
  192. if (typeof callable !== 'undefined') {
  193. // Simplest case: handler is passed, remove that listener ONLY.
  194. if (!Object.keys(storeElementEvent).length) {
  195. return
  196. }
  197. removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)
  198. return
  199. }
  200. if (isNamespace) {
  201. for (const elementEvent of Object.keys(events)) {
  202. removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1))
  203. }
  204. }
  205. for (const [keyHandlers, event] of Object.entries(storeElementEvent)) {
  206. const handlerKey = keyHandlers.replace(stripUidRegex, '')
  207. if (!inNamespace || originalTypeEvent.includes(handlerKey)) {
  208. removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
  209. }
  210. }
  211. },
  212. trigger(element, event, args) {
  213. if (typeof event !== 'string' || !element) {
  214. return null
  215. }
  216. const $ = getjQuery()
  217. const typeEvent = getTypeEvent(event)
  218. const inNamespace = event !== typeEvent
  219. let jQueryEvent = null
  220. let bubbles = true
  221. let nativeDispatch = true
  222. let defaultPrevented = false
  223. if (inNamespace && $) {
  224. jQueryEvent = $.Event(event, args)
  225. $(element).trigger(jQueryEvent)
  226. bubbles = !jQueryEvent.isPropagationStopped()
  227. nativeDispatch = !jQueryEvent.isImmediatePropagationStopped()
  228. defaultPrevented = jQueryEvent.isDefaultPrevented()
  229. }
  230. const evt = hydrateObj(new Event(event, { bubbles, cancelable: true }), args)
  231. if (defaultPrevented) {
  232. evt.preventDefault()
  233. }
  234. if (nativeDispatch) {
  235. element.dispatchEvent(evt)
  236. }
  237. if (evt.defaultPrevented && jQueryEvent) {
  238. jQueryEvent.preventDefault()
  239. }
  240. return evt
  241. }
  242. }
  243. function hydrateObj(obj, meta = {}) {
  244. for (const [key, value] of Object.entries(meta)) {
  245. try {
  246. obj[key] = value
  247. } catch {
  248. Object.defineProperty(obj, key, {
  249. configurable: true,
  250. get() {
  251. return value
  252. }
  253. })
  254. }
  255. }
  256. return obj
  257. }
  258. export default EventHandler