├── LICENSE ├── README.md ├── index.js ├── package.json └── test └── index.html /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright © 2024-today, Andrea Giammarchi, @WebReflection 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the “Software”), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included 13 | in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 21 | IN THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # add-promise-listener 2 | 3 | **Social Media Photo by [Europeana](https://unsplash.com/@europeanaeu) on [Unsplash](https://unsplash.com/)** 4 | 5 | Using promises as generic event listener. 6 | 7 | ```js 8 | import addPromiseListener from 'https://esm.run/add-promise-listener'; 9 | 10 | const button = document.getElementById('test-button'); 11 | const ac = new AbortController; 12 | addPromiseListener( 13 | button, 14 | 'click', 15 | { 16 | // this is optionally needed to be sure the operation is performed 17 | // when it's needed and not during the next tick: 18 | // stopPropagation: true 19 | // stopImmediatePropagation: true 20 | preventDefault: true, 21 | // optional signal to eventually catch rejections 22 | signal: ac.signal 23 | // other standard options are allowed as well 24 | // capture: true 25 | // passive: true 26 | } 27 | ).then( 28 | event => { 29 | console.log(`${event.type}ed 🥳`); 30 | console.assert(event.currentTarget === button, 'currentTarget'); 31 | console.assert(event.defaultPrevented, 'defaultPrevented'); 32 | }, 33 | event => { 34 | console.assert(event.currentTarget === button, 'currentTarget'); 35 | console.error(event.target.reason); 36 | } 37 | ); 38 | 39 | // simulate a rejection in 5 seconds 40 | setTimeout(() => ac.abort('timeout!'), 5000); 41 | ``` 42 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const { defineProperty } = Object; 2 | const { get } = Reflect; 3 | 4 | const methods = [ 5 | 'preventDefault', 6 | 'stopPropagation', 7 | 'stopImmediatePropagation', 8 | ]; 9 | 10 | const once = { once: true }; 11 | 12 | // avoid event.preventDefault throwing due illegal Proxy invocation 13 | const bound = (e, value) => typeof value === 'function' ? value.bind(e) : value; 14 | 15 | // traps the `event.currentTarget` to be sure it's available later on 16 | class Handler { 17 | #currentTarget; 18 | constructor(currentTarget) { 19 | this.#currentTarget = currentTarget; 20 | } 21 | get(e, name) { 22 | // Did you know? event.currentTarget disappears from events on 23 | // next tick, which is why this proxy handler needs to exist. 24 | return name === 'currentTarget' ? this.#currentTarget : bound(e, get(e, name)); 25 | } 26 | } 27 | 28 | /** 29 | * Add a listener that result as a Promise, fulfilled when the event happens once or rejected if the optional provided signal is aborted. 30 | * @param {Element} element 31 | * @param {string} type 32 | * @param {{ signal?:AbortSignal, capture?:boolean, passive?:boolean, preventDefault?:boolean, stopPropagation?:boolean, stopImmediatePropagation?:boolean }?} options 33 | * @returns {Promise} 34 | */ 35 | export default (element, type, options = null) => new Promise( 36 | (resolve, reject) => { 37 | const handler = new Handler(element); 38 | if (options.signal) { 39 | const abort = event => reject(new Proxy(event, handler)); 40 | options.signal.addEventListener('abort', abort, once); 41 | if (options.signal.aborted) 42 | return options.signal.dispatchEvent(new Event('abort')); 43 | } 44 | element.addEventListener( 45 | type, 46 | (event) => { 47 | for (const method of methods) { 48 | if (options[method]) event[method](); 49 | } 50 | resolve(new Proxy(event, handler)); 51 | }, 52 | { ...options, ...once } 53 | ); 54 | } 55 | ); 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "add-promise-listener", 3 | "version": "0.1.3", 4 | "main": "index.js", 5 | "directories": { 6 | "test": "test" 7 | }, 8 | "scripts": { 9 | "test": "echo -e \"\\x1b[1mhttp://localhost:8080/test/\\x1b[0m ↗️\"; npx static-handler ." 10 | }, 11 | "files": [ 12 | "index.js", 13 | "LICENSE", 14 | "README.md" 15 | ], 16 | "keywords": [ 17 | "promise", 18 | "listener" 19 | ], 20 | "author": "Andrea Giammarchi", 21 | "license": "MIT", 22 | "description": "Using promises as generic event listener", 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/WebReflection/add-promise-listener.git" 26 | }, 27 | "bugs": { 28 | "url": "https://github.com/WebReflection/add-promise-listener/issues" 29 | }, 30 | "homepage": "https://github.com/WebReflection/add-promise-listener#readme" 31 | } 32 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 38 | 39 | 40 | 41 | 42 | 43 | --------------------------------------------------------------------------------