Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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