@danielkalen/simplybind
Version:
Magically simple, framework-less one-way/two-way data binding for frontend/backend in ~5kb.
202 lines (163 loc) • 5.13 kB
JavaScript
import {DOM} from 'aurelia-pal';
//Note: path and deepPath are designed to handle v0 and v1 shadow dom specs respectively
function findOriginalEventTarget(event) {
return (event.path && event.path[0]) || (event.deepPath && event.deepPath[0]) || event.target;
}
function interceptStopPropagation(event) {
event.standardStopPropagation = event.stopPropagation;
event.stopPropagation = function() {
this.propagationStopped = true;
this.standardStopPropagation();
};
}
function handleDelegatedEvent(event) {
let interceptInstalled = false;
event.propagationStopped = false;
let target = findOriginalEventTarget(event);
while (target && !event.propagationStopped) {
if (target.delegatedCallbacks) {
let callback = target.delegatedCallbacks[event.type];
if (callback) {
if (!interceptInstalled) {
interceptStopPropagation(event);
interceptInstalled = true;
}
callback(event);
}
}
target = target.parentNode;
}
}
class DelegateHandlerEntry {
constructor(eventName) {
this.eventName = eventName;
this.count = 0;
}
increment() {
this.count++;
if (this.count === 1) {
DOM.addEventListener(this.eventName, handleDelegatedEvent, false);
}
}
decrement() {
this.count--;
if (this.count === 0) {
DOM.removeEventListener(this.eventName, handleDelegatedEvent);
}
}
}
class DefaultEventStrategy {
delegatedHandlers = {};
subscribe(target, targetEvent, callback, delegate) {
if (delegate) {
let delegatedHandlers = this.delegatedHandlers;
let handlerEntry = delegatedHandlers[targetEvent] || (delegatedHandlers[targetEvent] = new DelegateHandlerEntry(targetEvent));
let delegatedCallbacks = target.delegatedCallbacks || (target.delegatedCallbacks = {});
handlerEntry.increment();
delegatedCallbacks[targetEvent] = callback;
return function() {
handlerEntry.decrement();
delegatedCallbacks[targetEvent] = null;
};
}
target.addEventListener(targetEvent, callback, false);
return function() {
target.removeEventListener(targetEvent, callback);
};
}
}
export class EventManager {
constructor() {
this.elementHandlerLookup = {};
this.eventStrategyLookup = {};
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();
}
registerElementConfig(config) {
let tagName = config.tagName.toLowerCase();
let properties = config.properties;
let propertyName;
this.elementHandlerLookup[tagName] = {};
for (propertyName in properties) {
if (properties.hasOwnProperty(propertyName)) {
this.registerElementPropertyConfig(tagName, propertyName, properties[propertyName]);
}
}
}
registerElementPropertyConfig(tagName, propertyName, events) {
this.elementHandlerLookup[tagName][propertyName] = this.createElementHandler(events);
}
createElementHandler(events) {
return {
subscribe(target, callback) {
events.forEach(changeEvent => {
target.addEventListener(changeEvent, callback, false);
});
return function() {
events.forEach(changeEvent => {
target.removeEventListener(changeEvent, callback);
});
};
}
};
}
registerElementHandler(tagName, handler) {
this.elementHandlerLookup[tagName.toLowerCase()] = handler;
}
registerEventStrategy(eventName, strategy) {
this.eventStrategyLookup[eventName] = strategy;
}
getElementHandler(target, propertyName) {
let tagName;
let lookup = this.elementHandlerLookup;
if (target.tagName) {
tagName = target.tagName.toLowerCase();
if (lookup[tagName] && lookup[tagName][propertyName]) {
return lookup[tagName][propertyName];
}
if (propertyName === 'textContent' || propertyName === 'innerHTML') {
return lookup['content editable'].value;
}
if (propertyName === 'scrollTop' || propertyName === 'scrollLeft') {
return lookup['scrollable element'][propertyName];
}
}
return null;
}
addEventListener(target, targetEvent, callback, delegate) {
return (this.eventStrategyLookup[targetEvent] || this.defaultEventStrategy)
.subscribe(target, targetEvent, callback, delegate);
}
}