UNPKG

@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
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); } }