UNPKG

@serenity-is/corelib

Version:
304 lines (247 loc) 10 kB
/** * -------------------------------------------------------------------------- * Adapted from: Bootstrap dom/event-handler.js * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) * -------------------------------------------------------------------------- */ import { getjQuery } from "./environment" const stripNameRegex = /\..*/ const stripUidRegex = /::\d+$/ let uidEvent = 1 const customEvents: Record<string, string> = { mouseenter: 'mouseover', mouseleave: 'mouseout' } function makeEventUid(prefix: string): string { return `${prefix}::${uidEvent++}`; } type EventHandler = Function & { callable: EventListenerOrEventListenerObject; delegationSelector?: string | Function; oneOff?: boolean; uidEvent?: string; } type EventHandlers = Record<string, EventHandler>; type ElementEvents = Record<string, EventHandlers>; const eventRegistry: WeakMap<EventTarget, ElementEvents> = new WeakMap(); export function disposeDescendants(element: Element) { element.querySelectorAll("*").forEach(node => disposeElement(node)); } export function disposeElement(element: EventTarget): void { let events = eventRegistry.get(element); if (!events) return; eventRegistry.delete(element); var disposeHandlers = events["disposing"]; if (disposeHandlers) { for (const [_, handler] of Object.entries(disposeHandlers)) { if (typeof handler.callable === "function") { try { handler.callable.call(element, { target: element }); } catch { } } } } for (const [typeEvent, handlers] of Object.entries(events)) { for (const [handlerKey, handler] of Object.entries(handlers)) { element.removeEventListener(typeEvent, handler as any, Boolean(handler.delegationSelector)); delete handlers[handlerKey]; } } } function getElementEvents(element: EventTarget): ElementEvents { var events = eventRegistry.get(element); if (!events) eventRegistry.set(element, events = {}); return events; } function hydrateEvent(obj: Event, meta = {}) { for (const [key, value] of Object.entries(meta)) { if (key === 'bubbles' || key === 'cancelable') { // these should be set when the event is constructed continue; } try { (obj as any)[key] = value } catch { Object.defineProperty(obj, key, { configurable: true, get() { return value } }) } } return obj; } function baseHandler(element: EventTarget, fn: any) { return function handler(event: Event) { hydrateEvent(event, { delegateTarget: element }) if ((handler as any).oneOff) { removeListener(element, event.type, fn) } return fn.apply(element, [event]) } } function delegationHandler(element: EventTarget, selector: string, fn: Function) { return function handler(event: Event) { const domElements = (element as any).querySelectorAll(selector) for (let { target } = event; target && target !== this; target = (target as any).parentNode) { for (const domElement of (domElements as any)) { if (domElement !== target) { continue } hydrateEvent(event, { delegateTarget: target }) if ((handler as any).oneOff) { removeListener(element, event.type, selector, fn) } return fn.apply(target, [event]) } } } } function findHandler(handlers: EventHandlers, callable: Function, delegationSelector: any = null) { return Object.values(handlers) .find((event) => event.callable === callable && event.delegationSelector === delegationSelector) } function normalizeParameters(originalTypeEvent: string, handler: any, delegationFunction: any) { const isDelegated = typeof handler === 'string' const callable = isDelegated ? delegationFunction : (handler || delegationFunction); let typeEvent = getTypeEvent(originalTypeEvent) if (originalTypeEvent.indexOf(".bs.") >= 0) typeEvent = originalTypeEvent; return [isDelegated, callable, typeEvent] } export function addListener(element: EventTarget, originalTypeEvent: string, handler: Function | string, delegationFunction?: Function, oneOff?: boolean) { if (typeof originalTypeEvent !== 'string' || !element) { return; } const $ = getjQuery(); if ($) { let $element = $(element); if (oneOff) $element.one(originalTypeEvent, handler, delegationFunction); else $element.on(originalTypeEvent, handler, delegationFunction); return; } let [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationFunction); if (!callable) return; if (originalTypeEvent in customEvents) { const wrapFunction = (fn: Function) => { return function (event: Event & { relatedTarget?: any, delegateTarget: any }) { if (!event.relatedTarget || (event.relatedTarget !== event.delegateTarget && !event.delegateTarget.contains(event.relatedTarget))) { return fn.call(this, event) } } } callable = wrapFunction(callable) } const events = getElementEvents(element); const handlers = events[typeEvent] || (events[typeEvent] = Object.create(null)); const previousFunction = findHandler(handlers, callable, isDelegated ? handler : null); if (previousFunction) { (previousFunction as any).oneOff = (previousFunction as any).oneOff && oneOff return; } const dotIdx = originalTypeEvent.indexOf('.'); const ns = dotIdx > -1 ? originalTypeEvent.substring(dotIdx + 1) : ''; const uid = makeEventUid(ns) const fn = (isDelegated ? delegationHandler(element, handler as string, callable) : baseHandler(element, callable)) as any as EventHandler; fn.delegationSelector = isDelegated ? handler : null; fn.callable = callable; fn.oneOff = oneOff; fn.uidEvent = uid; if (uid !== '__proto__') handlers[uid] = fn; element.addEventListener(typeEvent, fn as any, isDelegated); } function removeHandler(element: EventTarget, events: ElementEvents, typeEvent: string, handler: any, delegationSelector: string | Function) { const fn = findHandler(events[typeEvent], handler, delegationSelector) if (!fn) { return } element.removeEventListener(typeEvent, fn as any, Boolean(delegationSelector)) delete events[typeEvent][(fn as any).uidEvent] } function removeNamespacedHandlers(element: EventTarget, events: ElementEvents, typeEvent: string, namespace: string) { const handlers = events[typeEvent] || {}; for (const [handlerKey, handler] of Object.entries(handlers)) { if (handlerKey.includes(namespace)) { removeHandler(element, events, typeEvent, handler.callable, handler.delegationSelector); } } } function getTypeEvent(event: string) { // allow to get the native events from namespaced events ('click.bs.button' --> 'click') event = event.replace(stripNameRegex, '') return customEvents[event] || event } export function removeListener(element: EventTarget, originalTypeEvent: string, handler?: any, delegationHandler?: Function): void { if (typeof originalTypeEvent !== 'string' || !element) { return } const $ = getjQuery(); if ($) { $(element).off(originalTypeEvent, handler, delegationHandler); return; } const [isDelegated, callable, typeEvent] = normalizeParameters(originalTypeEvent, handler, delegationHandler); const inNamespace = typeEvent !== originalTypeEvent; const events = getElementEvents(element); const handlers = events[typeEvent] || {}; const isNamespace = originalTypeEvent.startsWith('.'); if (typeof callable !== 'undefined') { if (!Object.keys(handlers).length) { return; } removeHandler(element, events, typeEvent, callable, isDelegated ? handler : null); return } if (isNamespace) { for (const elementEvent of Object.keys(events)) { removeNamespacedHandlers(element, events, elementEvent, originalTypeEvent.slice(1)); } } for (const [keyHandlers, handler] of Object.entries(handlers)) { const handlerKey = keyHandlers.replace(stripUidRegex, ''); if (!inNamespace || originalTypeEvent.includes(handlerKey)) { removeHandler(element, events, typeEvent, handler.callable, handler.delegationSelector); } } } export function triggerEvent(element: EventTarget, type: string, args?: any): Event & { isDefaultPrevented?(): boolean } { if (typeof type !== 'string' || !element) { return null; } const $ = getjQuery(); const typeEvent = getTypeEvent(type); const inNamespace = type !== typeEvent; let jQueryEvent = null; let bubbles = args?.bubbles ?? true; let nativeDispatch = true; let defaultPrevented = false; if (inNamespace && $) { jQueryEvent = $.Event(type, args); $(element).trigger(jQueryEvent); bubbles = bubbles && !jQueryEvent.isPropagationStopped(); nativeDispatch = !jQueryEvent.isImmediatePropagationStopped(); defaultPrevented = jQueryEvent.isDefaultPrevented(); } const evt = hydrateEvent(new Event(type, { bubbles, cancelable: args?.cancelable ?? true }), args); if (defaultPrevented) { evt.preventDefault(); } if (nativeDispatch) { element.dispatchEvent(evt); } if (evt.defaultPrevented && jQueryEvent) { jQueryEvent.preventDefault(); } return evt }