UNPKG

aurelia-binding

Version:

A modern databinding library for JavaScript and HTML.

388 lines (333 loc) 10.7 kB
// @ts-check import {DOM} from 'aurelia-pal'; import * as LogManager from 'aurelia-logging'; let emLogger = LogManager.getLogger('event-manager'); //Note: path and deepPath are designed to handle v0 and v1 shadow dom specs respectively function findOriginalEventTarget(event) { return (event.composedPath && event.composedPath()[0]) || (event.deepPath && event.deepPath()[0]) || (event.path && event.path[0]) || event.target; } function stopPropagation() { this.standardStopPropagation(); this.propagationStopped = true; } function handleCapturedEvent(event) { event.propagationStopped = false; let target = findOriginalEventTarget(event); let orderedCallbacks = []; /** * During capturing phase, event 'bubbles' down from parent. Needs to reorder callback from root down to target */ while (target) { if (target.capturedCallbacks) { let callback = target.capturedCallbacks[event.type]; if (callback) { if (event.stopPropagation !== stopPropagation) { event.standardStopPropagation = event.stopPropagation; event.stopPropagation = stopPropagation; } orderedCallbacks.push(callback); } } target = target.parentNode; } for (let i = orderedCallbacks.length - 1; i >= 0 && !event.propagationStopped; i--) { let orderedCallback = orderedCallbacks[i]; if ('handleEvent' in orderedCallback) { orderedCallback.handleEvent(event); } else { orderedCallback(event); } } } class CapturedHandlerEntry { constructor(eventName) { this.eventName = eventName; this.count = 0; } increment() { this.count++; if (this.count === 1) { DOM.addEventListener(this.eventName, handleCapturedEvent, true); } } decrement() { if (this.count === 0) { emLogger.warn('The same EventListener was disposed multiple times.'); } else if (--this.count === 0) { DOM.removeEventListener(this.eventName, handleCapturedEvent, true); } } } class DelegateHandlerEntry { constructor(eventName, eventManager) { this.eventName = eventName; this.count = 0; this.eventManager = eventManager; } handleEvent(event) { event.propagationStopped = false; let target = findOriginalEventTarget(event); while (target && !event.propagationStopped) { if (target.delegatedCallbacks) { let callback = target.delegatedCallbacks[event.type]; if (callback) { if (event.stopPropagation !== stopPropagation) { event.standardStopPropagation = event.stopPropagation; event.stopPropagation = stopPropagation; } if ('handleEvent' in callback) { callback.handleEvent(event); } else { callback(event); } } } const parent = target.parentNode; const shouldEscapeShadowRoot = this.eventManager.escapeShadowRoot && parent instanceof ShadowRoot; target = shouldEscapeShadowRoot ? parent.host : parent; } } increment() { this.count++; if (this.count === 1) { DOM.addEventListener(this.eventName, this, false); } } decrement() { if (this.count === 0) { emLogger.warn('The same EventListener was disposed multiple times.'); } else if (--this.count === 0) { DOM.removeEventListener(this.eventName, this, false); } } } /** * Enable dispose() pattern for `delegate` & `capture` commands */ class DelegationEntryHandler { /** * @param {DelegateHandlerEntry | CapturedHandlerEntry} entry * @param {Record<string, Function>} lookup * @param {string} targetEvent */ constructor(entry, lookup, targetEvent) { this.entry = entry; this.lookup = lookup; this.targetEvent = targetEvent; } dispose() { if (this.lookup[this.targetEvent]) { this.entry.decrement(); this.lookup[this.targetEvent] = null; } else { emLogger.warn('Calling .dispose() on already disposed eventListener'); } } } /** * Enable dispose() pattern for addEventListener for `trigger` */ class EventHandler { /** * @param {Element} target * @param {string} targetEvent * @param {EventListenerOrEventListenerObject} callback */ constructor(target, targetEvent, callback) { this.target = target; this.targetEvent = targetEvent; this.callback = callback; } dispose() { this.target.removeEventListener(this.targetEvent, this.callback); } } class DefaultEventStrategy { delegatedHandlers = {}; capturedHandlers = {}; constructor(eventManager) { this.eventManager = eventManager; } /** * @param {Element} target * @param {string} targetEvent * @param {EventListenerOrEventListenerObject} callback * @param {delegationStrategy} strategy * @param {boolean} disposable */ subscribe(target, targetEvent, callback, strategy, disposable) { let delegatedHandlers; let capturedHandlers; let handlerEntry; if (strategy === delegationStrategy.bubbling) { delegatedHandlers = this.delegatedHandlers; handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent, this.eventManager)); let delegatedCallbacks = target.delegatedCallbacks || (target.delegatedCallbacks = {}); if (!delegatedCallbacks[targetEvent]) { handlerEntry.increment(); } else { emLogger.warn('Overriding previous callback for event listener', {event: targetEvent, callback: callback, previousCallback: delegatedCallbacks[targetEvent]}); } delegatedCallbacks[targetEvent] = callback; if (disposable === true) { return new DelegationEntryHandler(handlerEntry, delegatedCallbacks, targetEvent); } return function() { handlerEntry.decrement(); delegatedCallbacks[targetEvent] = null; }; } if (strategy === delegationStrategy.capturing) { capturedHandlers = this.capturedHandlers; handlerEntry = capturedHandlers[targetEvent] || (capturedHandlers[targetEvent] = new CapturedHandlerEntry(targetEvent)); let capturedCallbacks = target.capturedCallbacks || (target.capturedCallbacks = {}); if (!capturedCallbacks[targetEvent]) { handlerEntry.increment(); } else { emLogger.error('already have a callback for event', {event: targetEvent, callback: callback}); } capturedCallbacks[targetEvent] = callback; if (disposable === true) { return new DelegationEntryHandler(handlerEntry, capturedCallbacks, targetEvent); } return function() { handlerEntry.decrement(); capturedCallbacks[targetEvent] = null; }; } target.addEventListener(targetEvent, callback); if (disposable === true) { return new EventHandler(target, targetEvent, callback); } return function() { target.removeEventListener(targetEvent, callback); }; } } export const delegationStrategy = { none: 0, capturing: 1, bubbling: 2 }; export class EventManager { constructor(escapeShadowRoot = false) { this.elementHandlerLookup = {}; this.eventStrategyLookup = {}; this.escapeShadowRoot = escapeShadowRoot; this.registerElementConfig({ tagName: 'input', properties: { value: ['change', 'input'], checked: ['change', 'input'], files: ['change', 'input'] } }); this.registerElementConfig({ tagName: 'textarea', properties: { value: ['change', 'input'] } }); this.registerElementConfig({ tagName: 'select', properties: { value: ['change'] } }); this.registerElementConfig({ tagName: 'content editable', properties: { value: ['change', 'input', 'blur', 'keyup', 'paste'] } }); this.registerElementConfig({ tagName: 'scrollable element', properties: { scrollTop: ['scroll'], scrollLeft: ['scroll'] } }); this.defaultEventStrategy = new DefaultEventStrategy(this); } registerElementConfig(config) { let tagName = config.tagName.toLowerCase(); let properties = config.properties; let propertyName; let lookup = this.elementHandlerLookup[tagName] = {}; for (propertyName in properties) { if (properties.hasOwnProperty(propertyName)) { lookup[propertyName] = properties[propertyName]; } } } registerEventStrategy(eventName, strategy) { this.eventStrategyLookup[eventName] = strategy; } /** * @param {Element | object} target * @param {string} propertyName */ getElementHandler(target, propertyName) { let tagName; let lookup = this.elementHandlerLookup; if (target.tagName) { tagName = target.tagName.toLowerCase(); if (lookup[tagName] && lookup[tagName][propertyName]) { return new EventSubscriber(lookup[tagName][propertyName]); } if (propertyName === 'textContent' || propertyName === 'innerHTML') { return new EventSubscriber(lookup['content editable'].value); } if (propertyName === 'scrollTop' || propertyName === 'scrollLeft') { return new EventSubscriber(lookup['scrollable element'][propertyName]); } } return null; } /** * @param {EventTarget} target * @param {string} targetEvent * @param {EventListenerOrEventListenerObject} callbackOrListener * @param {delegationStrategy} delegate * @param {boolean} disposable */ addEventListener(target, targetEvent, callbackOrListener, delegate, disposable) { return (this.eventStrategyLookup[targetEvent] || this.defaultEventStrategy) .subscribe(target, targetEvent, callbackOrListener, delegate, disposable); } } export class EventSubscriber { /** * @param {string[]} events */ constructor(events) { this.events = events; this.element = null; this.handler = null; } /** * @param {Element} element * @param {EventListenerOrEventListenerObject} callbackOrListener */ subscribe(element, callbackOrListener) { this.element = element; this.handler = callbackOrListener; let events = this.events; for (let i = 0, ii = events.length; ii > i; ++i) { element.addEventListener(events[i], callbackOrListener); } } dispose() { if (this.element === null) { // already disposed return; } let element = this.element; let callbackOrListener = this.handler; let events = this.events; for (let i = 0, ii = events.length; ii > i; ++i) { element.removeEventListener(events[i], callbackOrListener); } this.element = this.handler = null; } }