Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Assorted Event handler patches #36171

Merged
merged 7 commits into from May 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .bundlewatch.config.json
Expand Up @@ -50,7 +50,7 @@
},
{
"path": "./dist/js/bootstrap.js",
"maxSize": "28.25 kB"
"maxSize": "28.5 kB"
},
{
"path": "./dist/js/bootstrap.min.js",
Expand Down
89 changes: 42 additions & 47 deletions js/src/dom/event-handler.js
Expand Up @@ -74,12 +74,12 @@ const nativeEvents = new Set([
* Private methods
*/

function getUidEvent(element, uid) {
function makeEventUid(element, uid) {
return (uid && `${uid}::${uidEvent++}`) || element.uidEvent || uidEvent++
}

function getEvent(element) {
const uid = getUidEvent(element)
function getElementEvents(element) {
const uid = makeEventUid(element)

element.uidEvent = uid
eventRegistry[uid] = eventRegistry[uid] || {}
Expand Down Expand Up @@ -121,32 +121,30 @@ function bootstrapDelegationHandler(element, selector, fn) {
}
}

function findHandler(events, handler, delegationSelector = null) {
function findHandler(events, callable, delegationSelector = null) {
return Object.values(events)
.find(event => event.originalHandler === handler && event.delegationSelector === delegationSelector)
.find(event => event.callable === callable && event.delegationSelector === delegationSelector)
}

function normalizeParameters(originalTypeEvent, handler, delegationFunction) {
const delegation = typeof handler === 'string'
const originalHandler = delegation ? delegationFunction : handler
const isDelegated = typeof handler === 'string'
// todo: tooltip passes `false` instead of selector, so we need to check
const callable = isDelegated ? delegationFunction : (handler || delegationFunction)
let typeEvent = getTypeEvent(originalTypeEvent)

if (!nativeEvents.has(typeEvent)) {
typeEvent = originalTypeEvent
}

return [delegation, originalHandler, typeEvent]
return [isDelegated, callable, typeEvent]
}

function addHandler(element, originalTypeEvent, handler, delegationFunction, oneOff) {
if (typeof originalTypeEvent !== 'string' || !element) {
return
}

if (!handler) {
handler = delegationFunction
delegationFunction = null
}
let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)

// in case of mouseenter or mouseleave wrap the handler within a function that checks for its DOM position
// this prevents the handler from being dispatched the same way as mouseover or mouseout does
Expand All @@ -159,36 +157,31 @@ function addHandler(element, originalTypeEvent, handler, delegationFunction, one
}
}

if (delegationFunction) {
delegationFunction = wrapFunction(delegationFunction)
} else {
handler = wrapFunction(handler)
}
callable = wrapFunction(callable)
}

const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const events = getEvent(element)
const events = getElementEvents(element)
const handlers = events[typeEvent] || (events[typeEvent] = {})
const previousFunction = findHandler(handlers, originalHandler, delegation ? handler : null)
const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null)

if (previousFunction) {
previousFunction.oneOff = previousFunction.oneOff && oneOff

return
}

const uid = getUidEvent(originalHandler, originalTypeEvent.replace(namespaceRegex, ''))
const fn = delegation ?
bootstrapDelegationHandler(element, handler, delegationFunction) :
bootstrapHandler(element, handler)
const uid = makeEventUid(callable, originalTypeEvent.replace(namespaceRegex, ''))
const fn = isDelegated ?
bootstrapDelegationHandler(element, handler, callable) :
bootstrapHandler(element, callable)

fn.delegationSelector = delegation ? handler : null
fn.originalHandler = originalHandler
fn.delegationSelector = isDelegated ? handler : null
fn.callable = callable
fn.oneOff = oneOff
fn.uidEvent = uid
handlers[uid] = fn

element.addEventListener(typeEvent, fn, delegation)
element.addEventListener(typeEvent, fn, isDelegated)
}

function removeHandler(element, events, typeEvent, handler, delegationSelector) {
Expand All @@ -208,7 +201,7 @@ function removeNamespacedHandlers(element, events, typeEvent, namespace) {
for (const handlerKey of Object.keys(storeElementEvent)) {
if (handlerKey.includes(namespace)) {
const event = storeElementEvent[handlerKey]
removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
}
}
}
Expand All @@ -233,18 +226,19 @@ const EventHandler = {
return
}

const [delegation, originalHandler, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction)
const inNamespace = typeEvent !== originalTypeEvent
const events = getEvent(element)
const events = getElementEvents(element)
const storeElementEvent = events[typeEvent] || {}
const isNamespace = originalTypeEvent.startsWith('.')

if (typeof originalHandler !== 'undefined') {
if (typeof callable !== 'undefined') {
// Simplest case: handler is passed, remove that listener ONLY.
if (!events || !events[typeEvent]) {
if (!storeElementEvent) {
return
}

removeHandler(element, events, typeEvent, originalHandler, delegation ? handler : null)
removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null)
return
}

Expand All @@ -254,13 +248,12 @@ const EventHandler = {
}
}

const storeElementEvent = events[typeEvent] || {}
for (const keyHandlers of Object.keys(storeElementEvent)) {
const handlerKey = keyHandlers.replace(stripUidRegex, '')

if (!inNamespace || originalTypeEvent.includes(handlerKey)) {
const event = storeElementEvent[keyHandlers]
removeHandler(element, events, typeEvent, event.originalHandler, event.delegationSelector)
removeHandler(element, events, typeEvent, event.callable, event.delegationSelector)
}
}
},
Expand Down Expand Up @@ -288,18 +281,8 @@ const EventHandler = {
defaultPrevented = jQueryEvent.isDefaultPrevented()
}

const evt = new Event(event, { bubbles, cancelable: true })

// merge custom information in our event
if (typeof args !== 'undefined') {
for (const key of Object.keys(args)) {
Object.defineProperty(evt, key, {
get() {
return args[key]
}
})
}
}
let evt = new Event(event, { bubbles, cancelable: true })
evt = hydrateObj(evt, args)

if (defaultPrevented) {
evt.preventDefault()
Expand All @@ -317,4 +300,16 @@ const EventHandler = {
}
}

function hydrateObj(obj, meta) {
for (const [key, value] of Object.entries(meta || {})) {
Object.defineProperty(obj, key, {
get() {
return value
}
})
}

return obj
}

export default EventHandler