aurelia-binding
Version:
A modern databinding library for JavaScript and HTML.
388 lines (333 loc) • 10.7 kB
JavaScript
// @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;
}
}