├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── dist └── e.umd.js ├── examples ├── index.html └── test.js ├── package.json ├── src ├── e.d.ts ├── e.js ├── utils.d.ts └── utils.js ├── tsconfig.json └── webpack.mix.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "@babel/preset-env", 5 | { 6 | "useBuiltIns": "usage", 7 | "bugfixes": true, 8 | "corejs": 3 9 | } 10 | ] 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | mix-manifest.json 4 | examples 5 | 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | examples 2 | .idea/ 3 | node_modules/ 4 | mix-manifest.json 5 | *.map 6 | .babelrc 7 | *.mix.js 8 | vanilla.js -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | `E` is a library which combines a eventBus/emitter, DOM events management, delegated events, and event-based utils into a single lightweight and performant library. 2 | 3 | ![npm](https://img.shields.io/npm/v/@unseenco/e?style=flat-square) 4 | 5 | `E` works in all modern browsers (not IE11!). 6 | * [Getting started](#getting-started) 7 | * [Adding DOM Events](#dom-events) 8 | * [Adding Delegated events](#delegated-events) 9 | * [Removing events](#removing-events) 10 | * [The Event Bus](#event-bus) 11 | * [Binding handlers to maintain scope](#binding-handlers-to-maintain-scope) 12 | 13 | 14 | ## Getting started 15 | 16 | In order to use, just import it and go!: 17 | 18 | ```js 19 | import E from '@unseenco/e' 20 | ``` 21 | 22 | ## DOM Events 23 | 24 | The `on` method attaches an event to one or many DOM elements with an easy-to-use API: 25 | 26 | ```js 27 | E.on('click', '.js-open', callback) 28 | E.on('resize', window, callback) 29 | 30 | // Also accepts NodeLists/Arrays of elements 31 | E.on('click', document.querySelectorAll('.btn'), callback) 32 | 33 | // With a HTMLElement 34 | E.on('click', document.getElementById('unique'), callback) 35 | 36 | // You can also pass additional addEventListener options as a 4th param 37 | E.on('click', '#btn', callback, { passive: true }) 38 | ``` 39 | 40 | You can also add a callback to multiple events at once: 41 | ```js 42 | E.on('click keyup', '.js-open', callback) 43 | ``` 44 | 45 | 46 | 47 | ## Delegated Events 48 | Events bound with `delegate` are bound to the `document` instead of the element, which removes the need to rebind/remove events during page transitions, or when the DOM updates after load. 49 | 50 | Intercepted events are dispatched to the correct handler using [Selector Set](https://github.com/josh/selector-set), which matches the event target element [incredibly efficiently](https://github.com/josh/selector-set#inspired-by-browsers). 51 | 52 | The `delegate` method currently only accepts a selector string to match elements: 53 | ```js 54 | E.delegate('click', '.js-open', callback) 55 | ``` 56 | 57 | You can delegate a callback to multiple events at once: 58 | ```js 59 | E.delegate('input keyup', '.js-input', callback) 60 | ``` 61 | 62 | ## Removing Events 63 | You can remove a bound handler using the `off` method. The arguments are exactly the same as the `on` method, and events can be removed by passing a `string`, `HTMLElement`, or a `NodeList`. 64 | 65 | ```js 66 | E.off('click', '.js-open', callback) 67 | ``` 68 | 69 | If an element has the same callback for multiple events, you can remove them all at once: 70 | ```js 71 | E.off('click focus', '.js-open', callback) 72 | ``` 73 | 74 | ## Event Bus 75 | The API for the event bus uses the exact same methods as above, but without supplying a DOM element. 76 | 77 | #### Registering a bus event 78 | Use the `on` method to register an event and a listener. As many listeners can be subscribed to your event as you like. 79 | ```js 80 | E.on('my.bus.event', callback) 81 | ``` 82 | 83 | #### Emitting a bus event 84 | Use the `emit` method without an element will attempt to dispatch a bus event. If one exists, all listeners will be run in the order they were originally added: 85 | ```js 86 | E.emit('my.bus.event') 87 | 88 | // you can also pass arguments through 89 | E.emit('my.bus.event', arg1, arg2) 90 | ``` 91 | 92 | #### Removing a listener from a bus event 93 | You can subscribe one or all events from the bus using `off`: 94 | 95 | ```js 96 | // Will remove the supplied callback if found 97 | E.off('my.bus.event', callback) 98 | 99 | // Will remove all listeners for the bus event 100 | E.off('my.bus.event') 101 | ``` 102 | 103 | ### Debugging 104 | ```js 105 | // returns a object containing the current bus events registered 106 | E.debugBus() 107 | 108 | // returns a boolean indicating if the event has listeners or not 109 | E.hasBus('my.bus.event') 110 | ``` 111 | 112 | ## Binding handlers to maintain scope 113 | There are many ways to ensure that your event handlers keep the correct context when working with OO. 114 | 115 | #### Closure method (preferred) 116 | 117 | Probably the simplest method way to keep scope in handlers is to use ES6: 118 | 119 | ```js 120 | class Foo { 121 | bar = (e) => { 122 | console.log(this) 123 | } 124 | } 125 | ``` 126 | 127 | #### Using `bindAll` 128 | 129 | `Unseen.e` has a handy `bindAll` method if you prefer to do it the old-fashioned way: 130 | ```js 131 | class Foo { 132 | constructor() { 133 | E.bindAll(this, ['bar']) 134 | } 135 | 136 | bar() { 137 | console.log(this) 138 | } 139 | } 140 | ``` 141 | 142 | You can also call `bindAll` without providing any methods to automatically bind all public methods to the current instance: 143 | 144 | ```js 145 | class Foo { 146 | constructor() { 147 | // Will bind bar, but not privateBar 148 | E.bindAll(this) 149 | } 150 | 151 | bar() { 152 | console.log(this) 153 | } 154 | 155 | #privateBar() { 156 | 157 | } 158 | } 159 | ``` -------------------------------------------------------------------------------- /dist/e.umd.js: -------------------------------------------------------------------------------- 1 | !function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t():"function"==typeof define&&define.amd?define("E",[],t):"object"==typeof exports?exports.E=t():e.E=t()}(self,(function(){return(()=>{"use strict";var e={d:(t,n)=>{for(var r in n)e.o(n,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:n[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t),r:e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})}},t={};function n(){if(!(this instanceof n))return new n;this.size=0,this.uid=0,this.selectors=[],this.selectorObjects={},this.indexes=Object.create(this.indexes),this.activeIndexes=[]}e.r(t),e.d(t,{default:()=>k});var r=window.document.documentElement,o=r.matches||r.webkitMatchesSelector||r.mozMatchesSelector||r.oMatchesSelector||r.msMatchesSelector;n.prototype.matchesSelector=function(e,t){return o.call(e,t)},n.prototype.querySelectorAll=function(e,t){return t.querySelectorAll(e)},n.prototype.indexes=[];var i=/^#((?:[\w\u00c0-\uFFFF\-]|\\.)+)/g;n.prototype.indexes.push({name:"ID",selector:function(e){var t;if(t=e.match(i))return t[0].slice(1)},element:function(e){if(e.id)return[e.id]}});var a=/^\.((?:[\w\u00c0-\uFFFF\-]|\\.)+)/g;n.prototype.indexes.push({name:"CLASS",selector:function(e){var t;if(t=e.match(a))return t[0].slice(1)},element:function(e){var t=e.className;if(t){if("string"==typeof t)return t.split(/\s/);if("object"==typeof t&&"baseVal"in t)return t.baseVal.split(/\s/)}}});var s,u=/^((?:[\w\u00c0-\uFFFF\-]|\\.)+)/g;n.prototype.indexes.push({name:"TAG",selector:function(e){var t;if(t=e.match(u))return t[0].toUpperCase()},element:function(e){return[e.nodeName.toUpperCase()]}}),n.prototype.indexes.default={name:"UNIVERSAL",selector:function(){return!0},element:function(){return[!0]}},s="function"==typeof window.Map?window.Map:function(){function e(){this.map={}}return e.prototype.get=function(e){return this.map[e+" "]},e.prototype.set=function(e,t){this.map[e+" "]=t},e}();var l=/((?:\((?:\([^()]+\)|[^()]+)+\)|\[(?:\[[^\[\]]*\]|['"][^'"]*['"]|[^\[\]'"]+)+\]|\\.|[^ >+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g;function c(e,t){var n,r,o,i,a,s,u=(e=e.slice(0).concat(e.default)).length,c=t,f=[];do{if(l.exec(""),(o=l.exec(c))&&(c=o[3],o[2]||!c))for(n=0;ne.length)&&(t=e.length);for(var n=0,r=new Array(t);n1?t-1:0),r=1;r 2 | 3 | 4 | E test 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 |

e.target traversal

13 |
14 | 15 |

It should matter

16 | where you click 17 |
18 | 19 |

nodelist tests

20 |
21 | 22 | 23 | 24 |
25 | 26 |

Event Bus

27 | 28 | 29 | 30 | 31 | 32 |
33 |
34 |
35 |
36 | 37 | 38 | delegated blur focus 39 | delegated blur focus 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /examples/test.js: -------------------------------------------------------------------------------- 1 | import E from '../src/e' 2 | 3 | const btn = document.getElementById('btn') 4 | const btn2 = document.getElementById('btn2') 5 | 6 | 7 | class Foo { 8 | init() { 9 | E.bindAll(this) 10 | 11 | E.on('resize', window, e => console.log('window resized!')) 12 | E.on('click', btn, this.eventHandler, { capture: true }) 13 | E.off('click', btn, this.eventHandler, { capture: true }) 14 | E.on('click', btn2, this.offHandler) 15 | E.on('click', '#btnone', this.one, { once: true }) 16 | E.delegate('click', '#btn3', this.onceHandler) 17 | E.delegate('click', '.deep', this.delegateHandler) 18 | 19 | E.on('mouseenter', document.querySelectorAll('.nodelist'), () => console.log('nodelist')) 20 | E.on('mouseenter', [...document.querySelectorAll('.nodelist')], () => console.log('nodelist array')) 21 | 22 | E.delegate('mouseenter', '#mouseover', this.delegatedMouseEnter) 23 | E.delegate('mouseleave', '#mouseover', this.delegatedMouseLeave) 24 | //E.off('mouseenter', '#mouseover', this.delegatedMouseEnter) 25 | //E.off('mouseleave', '#mouseover', this.delegatedMouseLeave) 26 | 27 | E.delegate('blur focus', '.delegatedblurfocus', (e) => console.log(`delegated ${e.type}`)) 28 | 29 | E.delegate('click', 'button, h2', () => console.log('qs example')) 30 | 31 | // Event bus example 32 | E.on('event.bus.event event.bus.event2', this.listener) 33 | E.on('event.bus.removal', this.removalTest1) 34 | E.on('event.bus.removal', this.removalTest2) 35 | E.on('event.bus.removal', this.removalTest3) 36 | E.on('click', '#bus-test', this.triggerBus) 37 | E.on('click', '#bus-off', this.removeBus) 38 | 39 | console.log(E.hasBus('doesnt exist'), E.debugDelegated()) 40 | console.log(E.debugBus()) 41 | } 42 | 43 | delegatedMouseEnter() { 44 | console.log('delegated mouse enter') 45 | } 46 | 47 | delegatedMouseLeave() { 48 | console.log('delegated mouse leave') 49 | } 50 | 51 | one() { 52 | console.log('one!') 53 | } 54 | 55 | onceHandler(e) { 56 | console.log('delegated event target test', e) 57 | } 58 | 59 | eventHandler(e) { 60 | console.log('Dom event test', e) 61 | } 62 | 63 | delegateHandler(e) { 64 | console.log('delegated nested event target test', e) 65 | } 66 | 67 | removalTest1() { 68 | console.log('removal test 1') 69 | E.off('event.bus.removal', this.removalTest1) 70 | } 71 | 72 | removalTest2() { 73 | console.log('removal test 2') 74 | } 75 | 76 | removalTest3() { 77 | console.log('removal test 3') 78 | } 79 | 80 | triggerBus() { 81 | console.log('triggering event.bus.event event') 82 | E.emit('event.bus.event', 'one', 2) 83 | E.emit('event.bus.event2', 'two', 2) 84 | E.emit('event.bus.removal') 85 | } 86 | 87 | removeBus() { 88 | console.log('bus off') 89 | E.off('event.bus.event', this.listener) 90 | E.off('event.bus.imaginary') 91 | } 92 | 93 | listener(arg1, arg2) { 94 | console.log('Triggered via the event bus!', arg1, arg2) 95 | } 96 | 97 | offHandler = () => { 98 | E.off('click', btn, this.onceHandler) 99 | E.off('click', '#btn3', this.onceHandler) 100 | E.off('click', '#btnone', this.one) 101 | } 102 | } 103 | 104 | let bar = new Foo() 105 | bar.init() 106 | 107 | 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@unseenco/e", 3 | "version": "2.4.0", 4 | "description": "The complete (but tiny) js events solution - An event bus/emitter, simple DOM event API, and incredibly efficient delegated events.", 5 | "keywords": [ 6 | "eventbus", 7 | "bus", 8 | "events", 9 | "delegation", 10 | "delegated events", 11 | "DOM events", 12 | "binding", 13 | "events management", 14 | "event manager", 15 | "event bus", 16 | "tiny", 17 | "lightweight", 18 | "efficient", 19 | "emitter" 20 | ], 21 | "contributors": [ 22 | { 23 | "name": "Jake Whiteley", 24 | "email": "jake@unseen.co" 25 | } 26 | ], 27 | "homepage": "https://unseen.co", 28 | "license": "GPL-3.0-or-later", 29 | "publishConfig": { 30 | "access": "public" 31 | }, 32 | "repository": "https://github.com/craftedbygc/e", 33 | "main": "src/e.js", 34 | "dependencies": { 35 | "selector-set": "^1.1.5" 36 | }, 37 | "devDependencies": { 38 | "laravel-mix": "^6.0.0", 39 | "typescript": "^4.5.5" 40 | }, 41 | "scripts": { 42 | "dev": "npm run development", 43 | "development": "mix", 44 | "start": "npx mix watch", 45 | "prod": "npm run production", 46 | "production": "mix --production", 47 | "ts": "tsc" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/e.d.ts: -------------------------------------------------------------------------------- 1 | export default instance; 2 | declare const instance: E; 3 | /** 4 | * Public API 5 | */ 6 | declare class E { 7 | /** 8 | * Binds all provided methods to a provided context. 9 | * 10 | * @param {object} context 11 | * @param {string[]} [methods] Optional. 12 | */ 13 | bindAll(context: object, methods?: string[]): void; 14 | /** 15 | * Bind event to a string, NodeList, or element. 16 | * 17 | * @param {string} event 18 | * @param {string|NodeList|NodeListOf|HTMLElement|HTMLElement[]|Window|Document|function} el 19 | * @param {*} [callback] 20 | * @param {{}|boolean} [options] 21 | */ 22 | on(event: string, el: string | NodeList | NodeListOf | HTMLElement | HTMLElement[] | Window | Document | Function, callback?: any, options?: {} | boolean): void; 23 | /** 24 | * Add a delegated event. 25 | * 26 | * @param {string} event 27 | * @param {string|NodeList|HTMLElement|Element} delegate 28 | * @param {*} [callback] 29 | */ 30 | delegate(event: string, delegate: string | NodeList | HTMLElement | Element, callback?: any): void; 31 | /** 32 | * Remove a callback from a DOM element, or one or all Bus events. 33 | * 34 | * @param {string} event 35 | * @param {string|NodeList|HTMLElement|Element|Window|undefined} [el] 36 | * @param {*} [callback] 37 | * @param {{}|boolean} [options] 38 | */ 39 | off(event: string, el?: string | NodeList | HTMLElement | Element | Window | undefined, callback?: any, options?: {} | boolean): void; 40 | /** 41 | * Emit a Bus event. 42 | * 43 | * @param {string} event 44 | * @param {...*} args 45 | */ 46 | emit(event: string, ...args: any[]): void; 47 | /** 48 | * Return a clone of the delegated event stack for debugging. 49 | * 50 | * @returns {Object.} 51 | */ 52 | debugDelegated(): { 53 | [x: string]: any[]; 54 | }; 55 | /** 56 | * Return a clone of the bus event stack for debugging. 57 | * 58 | * @returns {Object.} 59 | */ 60 | debugBus(): { 61 | [x: string]: any[]; 62 | }; 63 | /** 64 | * Checks if a given bus event has listeners. 65 | * 66 | * @param {string} event 67 | * @returns {boolean} 68 | */ 69 | hasBus(event: string): boolean; 70 | } 71 | -------------------------------------------------------------------------------- /src/e.js: -------------------------------------------------------------------------------- 1 | import SelectorSet from 'selector-set' 2 | import { 3 | clone, 4 | eventTypes, 5 | handleDelegation, 6 | listeners, 7 | makeBusStack, 8 | maybeRunQuerySelector, 9 | nonBubblers, 10 | triggerBus 11 | } from './utils' 12 | 13 | /** 14 | * Public API 15 | */ 16 | class E { 17 | /** 18 | * Binds all provided methods to a provided context. 19 | * 20 | * @param {object} context 21 | * @param {string[]} [methods] Optional. 22 | */ 23 | bindAll(context, methods) { 24 | if (!methods) { 25 | methods = Object.getOwnPropertyNames(Object.getPrototypeOf(context)) 26 | } 27 | 28 | for (let i = 0; i < methods.length; i++) { 29 | context[methods[i]] = context[methods[i]].bind(context) 30 | } 31 | } 32 | 33 | /** 34 | * Bind event to a string, NodeList, or element. 35 | * 36 | * @param {string} event 37 | * @param {string|NodeList|NodeListOf|HTMLElement|HTMLElement[]|Window|Document|function} el 38 | * @param {*} [callback] 39 | * @param {{}|boolean} [options] 40 | */ 41 | on(event, el, callback, options) { 42 | const events = event.split(' ') 43 | 44 | for (let i = 0; i < events.length; i++) { 45 | if (typeof el === 'function' && callback === undefined) { 46 | makeBusStack(events[i]) 47 | listeners[events[i]].add(el) 48 | continue 49 | } 50 | 51 | if (el.nodeType && el.nodeType === 1 || el === window || el === document) { 52 | el.addEventListener(events[i], callback, options) 53 | continue 54 | } 55 | 56 | el = maybeRunQuerySelector(el) 57 | 58 | for (let n = 0; n < el.length; n++) { 59 | el[n].addEventListener(events[i], callback, options) 60 | } 61 | } 62 | } 63 | 64 | /** 65 | * Add a delegated event. 66 | * 67 | * @param {string} event 68 | * @param {string|NodeList|HTMLElement|Element} delegate 69 | * @param {*} [callback] 70 | */ 71 | delegate(event, delegate, callback) { 72 | const events = event.split(' ') 73 | 74 | for (let i = 0; i < events.length; i++) { 75 | let map = eventTypes[events[i]] 76 | 77 | if (map === undefined) { 78 | map = new SelectorSet() 79 | eventTypes[events[i]] = map 80 | 81 | if (nonBubblers.indexOf(events[i]) !== -1) { 82 | document.addEventListener(events[i], handleDelegation, true) 83 | } else { 84 | document.addEventListener(events[i], handleDelegation) 85 | } 86 | } 87 | 88 | map.add(delegate, callback) 89 | } 90 | } 91 | 92 | /** 93 | * Remove a callback from a DOM element, or one or all Bus events. 94 | * 95 | * @param {string} event 96 | * @param {string|NodeList|HTMLElement|Element|Window|undefined} [el] 97 | * @param {*} [callback] 98 | * @param {{}|boolean} [options] 99 | */ 100 | off(event, el, callback, options) { 101 | const events = event.split(' ') 102 | 103 | for (let i = 0; i < events.length; i++) { 104 | if (el === undefined) { 105 | listeners[events[i]]?.clear() 106 | continue 107 | } 108 | 109 | if (typeof el === 'function') { 110 | makeBusStack(events[i]) 111 | listeners[events[i]].delete(el) 112 | continue 113 | } 114 | 115 | const map = eventTypes[events[i]] 116 | 117 | if (map !== undefined) { 118 | map.remove(el, callback) 119 | 120 | if (map.size === 0) { 121 | delete eventTypes[events[i]] 122 | 123 | if (nonBubblers.indexOf(events[i]) !== -1) { 124 | document.removeEventListener(events[i], handleDelegation, true) 125 | } else { 126 | document.removeEventListener(events[i], handleDelegation) 127 | } 128 | continue 129 | } 130 | } 131 | 132 | if (el.removeEventListener !== undefined) { 133 | el.removeEventListener(events[i], callback, options) 134 | continue 135 | } 136 | 137 | el = maybeRunQuerySelector(el) 138 | 139 | for (let n = 0; n < el.length; n++) { 140 | el[n].removeEventListener(events[i], callback, options) 141 | } 142 | } 143 | } 144 | 145 | /** 146 | * Emit a Bus event. 147 | * 148 | * @param {string} event 149 | * @param {...*} args 150 | */ 151 | emit(event, ...args) { 152 | triggerBus(event, args) 153 | } 154 | 155 | /** 156 | * Return a clone of the delegated event stack for debugging. 157 | * 158 | * @returns {Object.} 159 | */ 160 | debugDelegated() { 161 | return JSON.parse(JSON.stringify(eventTypes)) 162 | } 163 | 164 | /** 165 | * Return a clone of the bus event stack for debugging. 166 | * 167 | * @returns {Object.} 168 | */ 169 | debugBus() { 170 | return clone(listeners) 171 | } 172 | 173 | /** 174 | * Checks if a given bus event has listeners. 175 | * 176 | * @param {string} event 177 | * @returns {boolean} 178 | */ 179 | hasBus(event) { 180 | return this.debugBus().hasOwnProperty(event) 181 | } 182 | } 183 | 184 | const instance = new E() 185 | export default instance 186 | -------------------------------------------------------------------------------- /src/utils.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Holds the SelectorSets for each event type 3 | * @type {{}} 4 | */ 5 | export const eventTypes: {}; 6 | /** 7 | * Holds Bus event stacks 8 | * @type {{}} 9 | */ 10 | export const listeners: {}; 11 | /** 12 | * Events that don't bubble 13 | * @type {string[]} 14 | */ 15 | export const nonBubblers: string[]; 16 | /** 17 | * Make a bus stack if not already created. 18 | * 19 | * @param {string} event 20 | */ 21 | export function makeBusStack(event: string): void; 22 | /** 23 | * Trigger a bus stack. 24 | * 25 | * @param {string} event 26 | * @param args 27 | */ 28 | export function triggerBus(event: string, args: any): void; 29 | /** 30 | * Maybe run querySelectorAll if input is a string. 31 | * 32 | * @param {HTMLElement|Element|string} el 33 | * @returns {NodeListOf} 34 | */ 35 | export function maybeRunQuerySelector(el: HTMLElement | Element | string): NodeListOf; 36 | /** 37 | * Handle delegated events 38 | * 39 | * @param {Event} e 40 | */ 41 | export function handleDelegation(e: Event): void; 42 | /** 43 | * Creates a deep clone of an object. 44 | * 45 | * @param object 46 | * @returns {Object.} 47 | */ 48 | export function clone(object: any): { 49 | [x: string]: any[]; 50 | }; 51 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Holds the SelectorSets for each event type 3 | * @type {{}} 4 | */ 5 | const eventTypes = {} 6 | 7 | /** 8 | * Holds Bus event stacks 9 | * @type {{}} 10 | */ 11 | const listeners = {} 12 | 13 | /** 14 | * Events that don't bubble 15 | * @type {string[]} 16 | */ 17 | const nonBubblers = ['mouseenter', 'mouseleave', 'pointerenter', 'pointerleave', 'blur', 'focus'] 18 | 19 | /** 20 | * Make a bus stack if not already created. 21 | * 22 | * @param {string} event 23 | */ 24 | function makeBusStack(event) { 25 | if (listeners[event] === undefined) { 26 | listeners[event] = new Set() 27 | } 28 | } 29 | 30 | /** 31 | * Trigger a bus stack. 32 | * 33 | * @param {string} event 34 | * @param args 35 | */ 36 | function triggerBus(event, args) { 37 | if (listeners[event]) { 38 | listeners[event].forEach(cb => { 39 | cb(...args) 40 | }) 41 | } 42 | } 43 | 44 | /** 45 | * Maybe run querySelectorAll if input is a string. 46 | * 47 | * @param {HTMLElement|Element|string} el 48 | * @returns {NodeListOf} 49 | */ 50 | function maybeRunQuerySelector(el) { 51 | return typeof el === 'string' ? document.querySelectorAll(el) : el 52 | } 53 | 54 | /** 55 | * Handle delegated events 56 | * 57 | * @param {Event} e 58 | */ 59 | function handleDelegation(e) { 60 | let matches = traverse(eventTypes[e.type], e.target) 61 | 62 | if (matches.length) { 63 | for (let i = 0; i < matches.length; i++) { 64 | for (let i2 = 0; i2 < matches[i].stack.length; i2++) { 65 | if (nonBubblers.indexOf(e.type) !== -1) { 66 | addDelegateTarget(e, matches[i].delegatedTarget) 67 | if (e.target === matches[i].delegatedTarget) { 68 | matches[i].stack[i2].data(e) 69 | } 70 | } else { 71 | addDelegateTarget(e, matches[i].delegatedTarget) 72 | matches[i].stack[i2].data(e) 73 | } 74 | } 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * Find a matching selector for delegation 81 | * 82 | * @param {SelectorSet} listeners 83 | * @param {HTMLElement|Element|EventTarget} target 84 | * @returns {[]} 85 | */ 86 | function traverse(listeners, target) { 87 | const queue = [] 88 | let node = target 89 | 90 | do { 91 | if (node.nodeType !== 1) { 92 | break 93 | } 94 | 95 | const matches = listeners.matches(node) 96 | 97 | if (matches.length) { 98 | queue.push({delegatedTarget: node, stack: matches}) 99 | } 100 | } while ((node = node.parentElement)) 101 | 102 | return queue 103 | } 104 | 105 | /** 106 | * Add delegatedTarget attribute to dispatched delegated events 107 | * 108 | * @param {Event} event 109 | * @param {HTMLElement|Element} delegatedTarget 110 | */ 111 | function addDelegateTarget(event, delegatedTarget) { 112 | Object.defineProperty(event, 'currentTarget', { 113 | configurable: true, 114 | enumerable: true, 115 | get: () => delegatedTarget 116 | }) 117 | } 118 | 119 | /** 120 | * Creates a deep clone of an object. 121 | * 122 | * @param object 123 | * @returns {Object.} 124 | */ 125 | function clone(object) { 126 | const copy = {} 127 | 128 | for (const key in object) { 129 | copy[key] = [...object[key]] 130 | } 131 | 132 | return copy 133 | } 134 | 135 | export { 136 | eventTypes, 137 | listeners, 138 | nonBubblers, 139 | makeBusStack, 140 | triggerBus, 141 | maybeRunQuerySelector, 142 | handleDelegation, 143 | clone 144 | } 145 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // Change this to match your project 3 | "include": [ 4 | "src/**/*.js" 5 | ], 6 | "compilerOptions": { 7 | "allowJs": true, 8 | "declaration": true, 9 | "emitDeclarationOnly": true 10 | // "outDir": "dist" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | 3 | mix.setPublicPath('/') 4 | 5 | 6 | if (!mix.inProduction()) { 7 | mix.js('examples/test.js', 'examples/test.min.js') 8 | } 9 | mix 10 | .webpackConfig({ 11 | output: { 12 | library: 'E', 13 | libraryTarget: 'umd', 14 | umdNamedDefine: true 15 | } 16 | }) 17 | .js('src/e.js', 'dist/e.umd.js') 18 | 19 | --------------------------------------------------------------------------------