├── .eslintrc.yml ├── .github └── workflows │ └── CI.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.yml ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── docs ├── migrating-to-v6.md └── reference.md ├── package-lock.json ├── package.json ├── scripts ├── empty.js ├── rollup.config.js └── test.ts ├── src ├── index.ts └── lib │ ├── dom-exception.ts │ ├── error-handler.ts │ ├── event-attribute-handler.ts │ ├── event-target.ts │ ├── event-wrapper.ts │ ├── event.ts │ ├── global.ts │ ├── legacy.ts │ ├── listener-list-map.ts │ ├── listener-list.ts │ ├── listener.ts │ ├── misc.ts │ ├── warning-handler.ts │ └── warnings.ts ├── test ├── default-error-handler.ts ├── default-warning-handler.ts ├── define-custom-event-target.ts ├── event-attribute.ts ├── event-target.ts ├── event.ts ├── fixtures │ ├── entrypoint.ts │ └── types.ts └── lib │ ├── abort-signal-stub.ts │ ├── count-event-listeners.ts │ └── setup-error-check.ts ├── tsconfig.json └── tsconfig ├── base.json ├── build.json ├── dts.json └── test.json /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | 3 | ignorePatterns: 4 | - "!.*" 5 | - /coverage 6 | - /dist 7 | - /node_modules 8 | - /test/fixtures/types.ts 9 | # parser raised unexpected error. 10 | - /src/lib/warnings.ts 11 | 12 | extends: 13 | - plugin:@mysticatea/es2020 14 | 15 | globals: 16 | console: "off" 17 | 18 | rules: 19 | no-console: error 20 | "@mysticatea/ts/explicit-member-accessibility": "off" 21 | "@mysticatea/prettier": "off" 22 | 23 | settings: 24 | node: 25 | tryExtensions: 26 | - .tsx 27 | - .ts 28 | - .mjs 29 | - .cjs 30 | - .js 31 | - .json 32 | - .node 33 | 34 | overrides: 35 | - files: scripts/** 36 | rules: 37 | no-console: "off" 38 | 39 | - files: src/** 40 | rules: 41 | # Avoid iteration because transpiled code will inflate much. 42 | no-restricted-syntax: [error, ForOfStatement] 43 | "@mysticatea/prefer-for-of": "off" 44 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | schedule: 8 | - cron: 0 0 * * 0 9 | 10 | jobs: 11 | static-analysis: 12 | name: Static Analysis 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v2 17 | - name: Install Node.js 14.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 14.x 21 | - name: Install Packages 22 | run: npm ci 23 | - name: Test 24 | run: npx run-s "test:{tsc,lint,format}" 25 | 26 | test-on-node: 27 | name: Test on Node.js 28 | runs-on: ubuntu-latest 29 | strategy: 30 | matrix: 31 | node: [14.x, 12.x, 10.x, "10.13.0"] 32 | steps: 33 | - name: Checkout 34 | uses: actions/checkout@v2 35 | - name: Install Node.js ${{ matrix.node }} 36 | uses: actions/setup-node@v1 37 | with: 38 | node-version: ${{ matrix.node }} 39 | - name: Install Packages 40 | run: npm ci 41 | - name: Test 42 | run: npm run test:mocha -- --only-node 43 | - name: Send Coverage 44 | run: npx codecov 45 | env: 46 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 47 | 48 | test-on-browser: 49 | name: Test on Browsers 50 | runs-on: ubuntu-latest 51 | steps: 52 | - name: Checkout 53 | uses: actions/checkout@v2 54 | - name: Install Node.js 14.x 55 | uses: actions/setup-node@v1 56 | with: 57 | node-version: 14.x 58 | - name: Install Packages 59 | run: | 60 | sudo apt-get install libbrotli1 libegl1 libopus0 libwoff1 \ 61 | libgstreamer-plugins-base1.0-0 libgstreamer1.0-0 \ 62 | libgstreamer-gl1.0-0 libgstreamer-plugins-bad1.0-0 libopenjp2-7 \ 63 | libwebpdemux2 libhyphen0 libgles2 gstreamer1.0-libav 64 | npm ci 65 | - name: Test 66 | run: npm run test:mocha -- --only-browsers 67 | - name: Send Coverage 68 | run: npx codecov 69 | env: 70 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 71 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /dist 3 | /node_modules 4 | /npm-debug.log 5 | /test.* 6 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | coverage 3 | dist 4 | node_modules 5 | LICENSE 6 | package-lock.json 7 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | overrides: 2 | - files: ["*.js", "*.ts"] 3 | options: 4 | tabWidth: 4 5 | semi: false 6 | trailingComma: all 7 | arrowParens: avoid 8 | endOfLine: lf 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["dbaeumer.vscode-eslint", "esbenp.prettier-vscode"] 3 | } 4 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.format.semicolons": "remove", 3 | 4 | "[javascript]": { 5 | "editor.codeActionsOnSave": ["source.fixAll.eslint"], 6 | "editor.defaultFormatter": "esbenp.prettier-vscode", 7 | "editor.formatOnSave": true 8 | }, 9 | "[json]": { 10 | "editor.codeActionsOnSave": [], 11 | "editor.defaultFormatter": "esbenp.prettier-vscode", 12 | "editor.formatOnSave": true 13 | }, 14 | "[jsonc]": { 15 | "editor.codeActionsOnSave": [], 16 | "editor.defaultFormatter": "esbenp.prettier-vscode", 17 | "editor.formatOnSave": true 18 | }, 19 | "[markdown]": { 20 | "editor.codeActionsOnSave": [], 21 | "editor.defaultFormatter": "esbenp.prettier-vscode", 22 | "editor.formatOnSave": true 23 | }, 24 | "[typescript]": { 25 | "editor.codeActionsOnSave": [ 26 | "source.fixAll.eslint", 27 | "source.organizeImports" 28 | ], 29 | "editor.defaultFormatter": "esbenp.prettier-vscode", 30 | "editor.formatOnSave": true 31 | }, 32 | "[yaml]": { 33 | "editor.codeActionsOnSave": [], 34 | "editor.defaultFormatter": "esbenp.prettier-vscode", 35 | "editor.formatOnSave": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Toru Nagashima 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 is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | 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 IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # event-target-shim 2 | 3 | [![npm version](https://img.shields.io/npm/v/event-target-shim.svg)](https://www.npmjs.com/package/event-target-shim) 4 | [![Downloads/month](https://img.shields.io/npm/dm/event-target-shim.svg)](http://www.npmtrends.com/event-target-shim) 5 | [![Build Status](https://github.com/mysticatea/event-target-shim/workflows/CI/badge.svg)](https://github.com/mysticatea/event-target-shim/actions) 6 | [![Coverage Status](https://codecov.io/gh/mysticatea/event-target-shim/branch/master/graph/badge.svg)](https://codecov.io/gh/mysticatea/event-target-shim) 7 | [![Dependency Status](https://david-dm.org/mysticatea/event-target-shim.svg)](https://david-dm.org/mysticatea/event-target-shim) 8 | 9 | An implementation of [WHATWG `EventTarget` interface](https://dom.spec.whatwg.org/#interface-eventtarget) and [WHATWG `Event` interface](https://dom.spec.whatwg.org/#interface-event). This implementation supports constructor, `passive`, `once`, and `signal`. 10 | 11 | This implementation is designed ... 12 | 13 | - Working fine on both browsers and Node.js. 14 | - TypeScript friendly. 15 | 16 | **Native Support Information:** 17 | 18 | | Feature | IE | Edge | Firefox | Chrome | Safari | Node.js | 19 | | :------------------------ | :-- | :--- | :------ | :----- | :----- | :------ | 20 | | `Event` constructor | ❌ | 12 | 11 | 15 | 6 | 15.4.0 | 21 | | `EventTarget` constructor | ❌ | 87 | 84 | 87 | 14 | 15.4.0 | 22 | | `passive` option | ❌ | 16 | 49 | 51 | 10 | 15.4.0 | 23 | | `once` option | ❌ | 16 | 50 | 55 | 10 | 15.4.0 | 24 | | `signal` option | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | 25 | 26 | --- 27 | 28 | ## 💿 Installation 29 | 30 | Use [npm](https://www.npmjs.com/) or a compatible tool. 31 | 32 | ``` 33 | npm install event-target-shim 34 | ``` 35 | 36 | ## 📖 Getting started 37 | 38 | ```js 39 | import { EventTarget, Event } from "event-target-shim"; 40 | 41 | // constructor (was added to the standard on 8 Jul 2017) 42 | const myNode = new EventTarget(); 43 | 44 | // passive flag (was added to the standard on 6 Jan 2016) 45 | myNode.addEventListener( 46 | "hello", 47 | (e) => { 48 | e.preventDefault(); // ignored and print warning on console. 49 | }, 50 | { passive: true } 51 | ); 52 | 53 | // once flag (was added to the standard on 15 Apr 2016) 54 | myNode.addEventListener("hello", listener, { once: true }); 55 | myNode.dispatchEvent(new Event("hello")); // remove the listener after call. 56 | 57 | // signal (was added to the standard on 4 Dec 2020) 58 | const ac = new AbortController(); 59 | myNode.addEventListener("hello", listener, { signal: ac.signal }); 60 | ac.abort(); // remove the listener. 61 | ``` 62 | 63 | - For browsers, there are two ways: 64 | - use a bundler such as [Webpack](https://webpack.js.org/) to bundle. If you want to support IE11, use `import {} from "event-target-shim/es5"` instead. It's a transpiled code by babel. It depends on `@baebl/runtime` (`^7.12.0`) package. 65 | - use CDN such as `unpkg.com`. For example, `` will define `EventTargetShim` global variable. 66 | - The `AbortController` class was added to the standard on 14 Jul 2017. If you want the shim of that, use [abort-controller](https://www.npmjs.com/package/abort-controller) package. 67 | 68 | ### Runnable Examples 69 | 70 | - [Basic Example](https://jsbin.com/dapuwomamo/1/edit?html,console) 71 | - [Basic Example (IE11)](https://jsbin.com/xigeyetipe/1/edit?html,console) 72 | 73 | ## 📚 API Reference 74 | 75 | See [docs/reference.md](docs/reference.md). 76 | 77 | ## 💥 Migrating to v6 78 | 79 | See [docs/migrating-to-v6.md](docs/migrating-to-v6.md). 80 | 81 | ## 📰 Changelog 82 | 83 | See [GitHub releases](https://github.com/mysticatea/event-target-shim/releases). 84 | 85 | ## 🍻 Contributing 86 | 87 | Contributing is welcome ❤️ 88 | 89 | Please use GitHub issues/PRs. 90 | 91 | ### Development tools 92 | 93 | - `npm install` installs dependencies for development. 94 | - `npm test` runs tests and measures code coverage. 95 | - `npm run watch:mocha` runs tests on each file change. 96 | -------------------------------------------------------------------------------- /docs/migrating-to-v6.md: -------------------------------------------------------------------------------- 1 | # 💥 Migrating to v6 2 | 3 | `event-target-shim` v6.0.0 contains large changes of API. 4 | 5 | - [CommonJS's default export is no longer `EventTarget`.](#-commonjss-default-export-is-no-longer-eventtarget) 6 | - [The function call support of `EventTarget` constructor was removed.](#-the-function-call-support-of-eventtarget-constructor-was-removed) 7 | - [Internal file structure was changed.](#-internal-file-structure-was-changed) 8 | - [\[Node.js\] Node.js version requirement was updated.](#-nodejs-nodejs-version-requirement-was-updated) 9 | - [\[Node.js\] Throwing errors from listeners now causes `uncaughtExcption` event.](#-nodejs-throwing-errors-from-listeners-now-causes-uncaughtexcption-event) 10 | - [\[Browsers\] ECMAScript version requirement was updated.](#-browsers-ecmascript-version-requirement-was-updated) 11 | - [\[Browsers\] Throwing errors from listeners now causes `error` event on `window`.](#-browsers-throwing-errors-from-listeners-now-causes-error-event-on-window) 12 | - [\[TypeScript\] TypeScript version requirement was updated.](#-typescript-typescript-version-requirement-was-updated) 13 | - [\[TypeScript\] The second type parameter of `EventTarget` was removed.](#-typescript-the-second-type-parameter-of-eventtarget-was-removed) 14 | 15 | ## ■ CommonJS's default export is no longer `EventTarget`. 16 | 17 | ```js 18 | // ❌ Before 19 | const EventTarget = require("event-target-shim"); 20 | 21 | // ✅ After 22 | const { EventTarget } = require("event-target-shim"); 23 | // or 24 | const { default: EventTarget } = require("event-target-shim"); 25 | ``` 26 | 27 | Because this package has multiple exports (`EventTarget`, `Event`, ...), so we have to modify the static members of `EventTarget` in order to support CommonJS's default export. I don't want to modify `EventTarget` for this purpose. 28 | 29 | This change doesn't affect to ESM syntax. 30 | I.e., `import EventTarget from "event-target-shim"` is OK. 31 | 32 | ## ■ The function call support of `EventTarget` constructor was removed. 33 | 34 | ```js 35 | // ❌ Before 36 | import { EventTarget } from "event-target-shim"; 37 | class DerivedClass extends EventTarget("foo", "bar") {} 38 | 39 | // ✅ After 40 | import { defineCustomEventTarget } from "event-target-shim"; 41 | class DerivedClass extends defineCustomEventTarget("foo", "bar") {} 42 | 43 | // Or define getters/setters of `onfoo`/`onbar` manually in your derived class. 44 | import { 45 | EventTarget, 46 | getEventAttributeValue, 47 | setEventAttributeValue, 48 | } from "event-target-shim"; 49 | class DerivedClass extends EventTarget { 50 | get onfoo() { 51 | return getEventAttributeValue(this, "foo"); 52 | } 53 | set onfoo(value) { 54 | setEventAttributeValue(this, "foo", value); 55 | } 56 | get onbar() { 57 | return getEventAttributeValue(this, "bar"); 58 | } 59 | set onbar(value) { 60 | setEventAttributeValue(this, "bar", value); 61 | } 62 | } 63 | ``` 64 | 65 | Because that was non-standard behavior and ES2015 class syntax cannot support it. 66 | 67 | ## ■ Internal file structure was changed. 68 | 69 | - (previous) → (now) 70 | - `dist/event-target-shim.mjs` → `index.mjs` 71 | - `dist/event-target-shim.js` → `index.js` 72 | - `dist/event-target-shim.umd.js` → `umd.js` 73 | 74 | And now the internal file structure is private by the `exports` field of `package.json`. 75 | 76 | ## ■ \[Node.js] Node.js version requirement was updated. 77 | 78 | Now this package requires Node.js **10.13.0** or later. 79 | 80 | Because Node.js v9 and older have been End-of-Life already. 81 | 10.13.0 is the first LTS version of v10 series. 82 | 83 | ## ■ \[Node.js] Throwing errors from listeners now causes `uncaughtExcption` event. 84 | 85 | If a registered listener threw an exception while event dispatching, it now emits an `uncaughtExcption` event by default. 86 | 87 | You can customize this behavior by `setErrorHandler` API. 88 | 89 | ```js 90 | import { setErrorHandler } from "event-target-shim"; 91 | 92 | // Only print errors. 93 | setErrorHandler((error) => { 94 | console.error(error); 95 | }); 96 | ``` 97 | 98 | ## ■ \[Browsers] ECMAScript version requirement was updated. 99 | 100 | Now this package requires **ES2018** or later. 101 | 102 | Because modern browsers have supported ES2018 widely. 103 | If you want to support IE11, use `event-target-shim/es5` that is a transpiled version. 104 | 105 | ```js 106 | import { EventTarget } from "event-target-shim/es5"; 107 | ``` 108 | 109 | ## ■ \[Browsers] Throwing errors from listeners now causes `error` event on `window`. 110 | 111 | If a registered listener threw an exception while event dispatching, it now dispatches an `error` event on `window` by default. 112 | 113 | You can customize this behavior by `setErrorHandler` API. 114 | 115 | ```js 116 | import { setErrorHandler } from "event-target-shim"; 117 | 118 | // Only print errors. 119 | setErrorHandler((error) => { 120 | console.error(error); 121 | }); 122 | ``` 123 | 124 | ## ■ \[TypeScript] TypeScript version requirement was updated. 125 | 126 | Now this package requires TypeScript **4.1** or later if you are using this package on TypeScript. 127 | 128 | Because this is using [Template Literal Types](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#template-literal-types). 129 | 130 | ## ■ \[TypeScript] The second type parameter of `EventTarget` was removed. 131 | 132 | ```js 133 | // ❌ Before 134 | import { EventTarget } from "event-target-shim"; 135 | interface MyEventTarget 136 | extends EventTarget<{ myevent: Event }, { onmyevent: Event }> {} 137 | 138 | // ✅ After 139 | import { EventTarget } from "event-target-shim"; 140 | interface MyEventTarget extends EventTarget<{ myevent: Event }> { 141 | onmyevent: EventTarget.CallbackFunction | null; 142 | } 143 | ``` 144 | 145 | Because the `EventTarget` object never have event attributes. Derived classes can define event attributes. Therefore, it was odd that the `EventTarget` interface had event attributes. 146 | -------------------------------------------------------------------------------- /docs/reference.md: -------------------------------------------------------------------------------- 1 | # 📚 API Reference 2 | 3 | ## ■ `EventTarget` 4 | 5 | ```js 6 | import { EventTarget } from "event-target-shim"; 7 | // or 8 | const { EventTarget } = require("event-target-shim"); 9 | ``` 10 | 11 | > The HTML Standard: [EventTarget interface](https://dom.spec.whatwg.org/#interface-eventtarget) 12 | 13 | ### ▶ constructor 14 | 15 | Create a new instance of the `EventTarget` class. 16 | 17 | There are no arguments. 18 | 19 | ### ▶ `eventTarget.addEventListener(type, callback, options)` 20 | 21 | Register an event listener. 22 | 23 | - `type` is a string. This is the event name to register. 24 | - `callback` is a function. This is the event listener to register. 25 | - `options` is an object `{ capture?: boolean; passive?: boolean; once?: boolean; signal?: AbortSignal }`. This is optional. 26 | - `capture` is the flag to register the event listener for capture phase. 27 | - `passive` is the flag to ignore `event.preventDefault()` method in the event listener. 28 | - `once` is the flag to remove this callback automatically after the first call. 29 | - `signal` is an `AbortSignal` object to remove this callback. You can use this option as alternative to the `eventTarget.removeEventListener(...)` method. 30 | 31 | ### ▶ `eventTarget.removeEventListener(type, callback, options)` 32 | 33 | Unregister an event listener. 34 | 35 | - `type` is a string. This is the event name to unregister. 36 | - `callback` is a function. This is the event listener to unregister. 37 | - `options` is an object `{ capture?: boolean }`. This is optional. 38 | - `capture` is the flag to register the event listener for capture phase. 39 | 40 | ### ▶ `eventTarget.dispatchEvent(event)` 41 | 42 | Dispatch an event. 43 | 44 | - `event` is a [Event](https://dom.spec.whatwg.org/#event) object to dispatch. 45 | 46 | ## ■ `Event` 47 | 48 | ```js 49 | import { Event } from "event-target-shim"; 50 | // or 51 | const { Event } = require("event-target-shim"); 52 | ``` 53 | 54 | > The HTML Standard: [Event interface](https://dom.spec.whatwg.org/#interface-event) 55 | 56 | See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Event#Properties) for details. 57 | 58 | ## ■ Event attribute handler 59 | 60 | ```js 61 | import { 62 | getEventAttributeValue, 63 | setEventAttributeValue, 64 | } from "event-target-shim"; 65 | // or 66 | const { 67 | getEventAttributeValue, 68 | setEventAttributeValue, 69 | } = require("event-target-shim"); 70 | ``` 71 | 72 | > Non-standard. 73 | 74 | You can define event attributes (e.g. `onclick`) on your derived classes of `EventTarget`. 75 | 76 | #### Example 77 | 78 | ```js 79 | import { 80 | EventTarget, 81 | getEventAttributeValue, 82 | setEventAttributeValue, 83 | } from "event-target-shim"; 84 | 85 | class AbortSignal extends EventTarget { 86 | constructor() { 87 | this.aborted = false; 88 | } 89 | 90 | // Define `onabort` property 91 | get onabort() { 92 | return getEventAttributeValue(this, "abort"); 93 | } 94 | set onabort(value) { 95 | setEventAttributeValue(this, "abort", value); 96 | } 97 | } 98 | ``` 99 | 100 | ## ■ Error handling 101 | 102 | ```js 103 | import { setErrorHandler, setWarningHandler } from "event-target-shim"; 104 | ``` 105 | 106 | > Non-standard. 107 | 108 | You can customize error/wanring behavior of `EventTarget`-shim. 109 | 110 | ### ▶ `setErrorHandler(handler)` 111 | 112 | Set your error handler. The error means exceptions that event listeners threw. 113 | 114 | The default handler is `undefined`. It dispatches an [ErrorEvent](https://developer.mozilla.org/ja/docs/Web/API/ErrorEvent) on `window` on browsers, or emits an [`uncaughtException` event](https://nodejs.org/api/process.html#process_event_uncaughtexception) on `process` on Node.js. 115 | 116 | The first argument of the error handler is a thrown error. 117 | 118 | #### Example 119 | 120 | ```js 121 | import { setErrorHandler } from "event-target-shim"; 122 | 123 | // Print log only. 124 | setErrorHandler((error) => { 125 | console.error(error); 126 | }); 127 | ``` 128 | 129 | ### ▶ `setWarningHandler(handler)` 130 | 131 | Set your warning handler. The warning is reported when `EventTarget` or `Event` doesn't throw any errors but ignores operations silently. 132 | 133 | The default handler is `undefined`. It prints warnings with the `console.warn` method. 134 | 135 | The first argument of the warning handler is a reported warning information. It has three properties: 136 | 137 | - `code` ... A warning code. Use it for i18n. 138 | - `message` ... The warning message in English. 139 | - `args` ... The array of arguments for replacing placeholders in the message. 140 | 141 | The warning handler will be called when... 142 | 143 | | Code | Description | 144 | | :------ | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 145 | | `"W01"` | `Event.prototype.initEvent` method was called while dispatching. In this case, the method does nothing. | 146 | | `"W02"` | `Event.prototype.cancelBubble` setter received a falsy value. In this case, the setter does nothing. | 147 | | `"W03"` | `Event.prototype.returnValue` setter received a truthy value. In this case, the setter does nothing. | 148 | | `"W04"` | `Event.prototype.preventDefault` method was called or `Event.prototype.returnValue` setter received a falsy value, but the event was not cancelable. In this case, the method or setter does nothing. | 149 | | `"W05"` | `Event.prototype.preventDefault` method was called or `Event.prototype.returnValue` setter received a falsy value, but that's in a passive listener. In this case, the method or setter does nothing. | 150 | | `"W06"` | `EventTarget.prototype.addEventListener` method received a listener that has been added already. In this case, the method does nothing. | 151 | | `"W07"` | `EventTarget.prototype.addEventListener` method received a listener that has been added already, and any of `passive`, `once`, and `signal` options are different between the existing listener and the ignored listener. In this case, the new options are abandoned. | 152 | | `"W08"` | `EventTarget.prototype.{addEventListener,removeEventListener}` methods received an invalid event listener. In this case, the methods ignore the listener. | 153 | | `"W09"` | `setEventAttributeValue` function received an invalid event attribute handler. If that was a primitive value, the function removes the current event attribute handler. Otherwise, the function adopts the listener, but the listener will never be called. | 154 | 155 | #### Example 156 | 157 | ```js 158 | import { setWarningHandler } from "event-target-shim"; 159 | 160 | // Print log only. 161 | setWarningHandler((warning) => { 162 | console.warn(warning.message, ...warning.args); 163 | }); 164 | ``` 165 | 166 | ## ■ \[TypeScript] Types 167 | 168 | The `EventTarget` and `Event` classes this package provides are compatible with the `EventTarget` and `Event` interfaces in the built-in `DOM` library of TypeScript. We can assign those each other. 169 | 170 | Additionally, the `EventTarget` and `Event` classes this package provides have some type parameters. 171 | 172 | ### ▶ `EventTarget` 173 | 174 | The `EventTarget` class has two type parameters. 175 | 176 | - `TEventMap` ... Optional. The event map. Keys are event types and each value is the type of `Event` class. Default is `Record`.
177 | This event map provides known event types. It's useful to infer the event types on `addEventListener` method. 178 | - `TMode` ... Optional. The mode of `EventTarget` type. This is `"standard"` or `"strict"`. Default is `"standard"`.
179 | If this is `"standard"`, the `EventTarget` type accepts unknown event types as well. It follows the standard.
180 | If this is `"strict"`, the `EventTarget` type accepts only known event types. It will protect the mistakes of giving wrong `Event` objects. On the other hand, the `EventTarget` type is not compatible to the standard. 181 | 182 | #### Example 183 | 184 | ```ts 185 | type AbortSignalEventMap = { 186 | abort: Event<"abort">; 187 | }; 188 | 189 | class AbortSignal extends EventTarget { 190 | // .... 191 | } 192 | 193 | type EventSourceEventMap = { 194 | close: Event<"close">; 195 | error: Event<"error">; 196 | message: MessageEvent; 197 | }; 198 | 199 | class EventSource extends EventTarget { 200 | // .... 201 | } 202 | 203 | type MyEventMap = { 204 | // .... 205 | }; 206 | 207 | class MyStuff extends EventTarget { 208 | // .... 209 | } 210 | ``` 211 | 212 | ### ▶ `Event` 213 | 214 | The `Event` class has a type parameter. 215 | 216 | - `T` ... Optional. The type of the `type` property. Default is `string`. 217 | 218 | #### Example 219 | 220 | ```ts 221 | const e = new Event("myevent"); 222 | const t: "myevent" = e.type; // the type of `type` property is `"myevent"`. 223 | ``` 224 | 225 | ### ▶ `getEventAttributeValue(target, type)`, `setEventAttributeValue(target, type, value)` 226 | 227 | The `getEventAttributeValue` and `setEventAttributeValue` functions have a type parameter. 228 | 229 | - `T` ... The type of the `Event` class. 230 | 231 | #### Example 232 | 233 | ```ts 234 | type AbortSignalEventMap = { 235 | abort: Event<"abort">; 236 | }; 237 | 238 | class AbortSignal extends EventTarget { 239 | // .... 240 | 241 | get onabort() { 242 | return getEventAttributeValue(this, "abort"); 243 | } 244 | set onabort(value) { 245 | setEventAttributeValue(this, "abort", value); 246 | } 247 | } 248 | ``` 249 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-target-shim", 3 | "version": "6.0.2", 4 | "description": "An implementation of WHATWG EventTarget interface.", 5 | "main": "index.js", 6 | "exports": { 7 | ".": { 8 | "import": "./index.mjs", 9 | "require": "./index.js" 10 | }, 11 | "./es5": { 12 | "import": "./es5.mjs", 13 | "require": "./es5.js" 14 | }, 15 | "./umd": "./umd.js", 16 | "./package.json": "./package.json" 17 | }, 18 | "files": [ 19 | "index.*", 20 | "es5.*", 21 | "umd.*" 22 | ], 23 | "engines": { 24 | "node": ">=10.13.0" 25 | }, 26 | "scripts": { 27 | "build": "run-s \"build:{clean,rollup,dts,meta}\"", 28 | "build:clean": "rimraf \"dist/*\"", 29 | "build:rollup": "rollup --config scripts/rollup.config.js", 30 | "build:dts": "dts-bundle-generator --project tsconfig/dts.json --out-file dist/index.d.ts src/index.ts && dts-bundle-generator --project tsconfig/dts.json --out-file dist/es5.d.ts src/index.ts", 31 | "build:meta": "cpx \"{LICENSE,package.json,README.md}\" dist/", 32 | "preversion": "npm test", 33 | "version": "npm run build", 34 | "postversion": "release", 35 | "test": "run-s \"test:{clean,tsc,lint,format,mocha}\"", 36 | "test:clean": "rimraf \"coverage/*\"", 37 | "test:tsc": "tsc -p tsconfig/build.json --noEmit", 38 | "test:lint": "eslint .", 39 | "test:format": "prettier --check .", 40 | "test:mocha": "ts-node scripts/test", 41 | "watch:mocha": "mocha --require ts-node/register/transpile-only --extensions ts --watch-files src,test --watch \"test/*.ts\"" 42 | }, 43 | "dependencies": {}, 44 | "devDependencies": { 45 | "@babel/core": "^7.12.10", 46 | "@babel/plugin-transform-runtime": "^7.12.10", 47 | "@babel/preset-env": "^7.12.11", 48 | "@mysticatea/eslint-plugin": "^13.0.0", 49 | "@mysticatea/spy": "^0.1.2", 50 | "@mysticatea/tools": "^0.1.1", 51 | "@rollup/plugin-babel": "^5.2.2", 52 | "@rollup/plugin-typescript": "^8.1.0", 53 | "@types/istanbul-lib-coverage": "^2.0.3", 54 | "@types/istanbul-lib-report": "^3.0.0", 55 | "@types/istanbul-lib-source-maps": "^4.0.1", 56 | "@types/istanbul-reports": "^3.0.0", 57 | "@types/mocha": "^8.2.0", 58 | "@types/rimraf": "^3.0.0", 59 | "assert": "^2.0.0", 60 | "babel-loader": "^8.2.2", 61 | "babel-plugin-istanbul": "^6.0.0", 62 | "buffer": "^6.0.3", 63 | "chalk": "^4.1.0", 64 | "codecov": "^3.8.1", 65 | "cpx": "^1.5.0", 66 | "dts-bundle-generator": "^5.5.0", 67 | "eslint": "^7.15.0", 68 | "istanbul-lib-coverage": "^3.0.0", 69 | "istanbul-lib-report": "^3.0.0", 70 | "istanbul-lib-source-maps": "^4.0.0", 71 | "istanbul-reports": "^3.0.2", 72 | "mocha": "^7.2.0", 73 | "npm-run-all": "^4.1.5", 74 | "path-browserify": "^1.0.1", 75 | "playwright": "^1.7.0", 76 | "prettier": "~2.2.1", 77 | "process": "^0.11.10", 78 | "rimraf": "^3.0.2", 79 | "rollup": "^2.35.1", 80 | "rollup-plugin-terser": "^7.0.2", 81 | "rollup-watch": "^4.3.1", 82 | "stream-browserify": "^3.0.0", 83 | "ts-loader": "^8.0.12", 84 | "ts-node": "^9.1.1", 85 | "tslib": "^2.0.3", 86 | "typescript": "~4.1.3", 87 | "url": "^0.11.0", 88 | "util": "^0.12.3", 89 | "webpack": "^5.11.0" 90 | }, 91 | "repository": { 92 | "type": "git", 93 | "url": "https://github.com/mysticatea/event-target-shim.git" 94 | }, 95 | "keywords": [ 96 | "w3c", 97 | "whatwg", 98 | "eventtarget", 99 | "event", 100 | "events", 101 | "shim" 102 | ], 103 | "author": "Toru Nagashima", 104 | "license": "MIT", 105 | "bugs": { 106 | "url": "https://github.com/mysticatea/event-target-shim/issues" 107 | }, 108 | "homepage": "https://github.com/mysticatea/event-target-shim", 109 | "funding": "https://github.com/sponsors/mysticatea", 110 | "sideEffects": false, 111 | "unpkg": "umd.js" 112 | } 113 | -------------------------------------------------------------------------------- /scripts/empty.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mysticatea/event-target-shim/a37993c95ed9cc1d7cb89358ea126ed4f95372e8/scripts/empty.js -------------------------------------------------------------------------------- /scripts/rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from "@rollup/plugin-babel" 2 | import typescript from "@rollup/plugin-typescript" 3 | import { terser } from "rollup-plugin-terser" 4 | 5 | const babelBaseConfig = { 6 | babelrc: false, 7 | presets: [ 8 | [ 9 | "@babel/env", 10 | { 11 | modules: false, 12 | targets: "IE 11", 13 | useBuiltIns: false, 14 | }, 15 | ], 16 | ], 17 | } 18 | 19 | function sourcemapPathTransform(path) { 20 | return path.startsWith("../") ? path.slice("../".length) : path 21 | } 22 | 23 | export default [ 24 | { 25 | input: "src/index.ts", 26 | output: { 27 | file: "dist/index.mjs", 28 | format: "es", 29 | sourcemap: true, 30 | sourcemapPathTransform, 31 | }, 32 | plugins: [typescript({ tsconfig: "tsconfig/build.json" })], 33 | }, 34 | { 35 | input: "src/index.ts", 36 | output: { 37 | exports: "named", 38 | file: "dist/index.js", 39 | format: "cjs", 40 | sourcemap: true, 41 | sourcemapPathTransform, 42 | }, 43 | plugins: [typescript({ tsconfig: "tsconfig/build.json" })], 44 | }, 45 | { 46 | external: id => id.startsWith("@babel/runtime/"), 47 | input: "dist/index.mjs", 48 | output: { 49 | file: "dist/es5.mjs", 50 | format: "es", 51 | }, 52 | plugins: [ 53 | babel({ 54 | ...babelBaseConfig, 55 | babelHelpers: "runtime", 56 | plugins: [["@babel/transform-runtime", { useESModules: true }]], 57 | }), 58 | ], 59 | }, 60 | { 61 | external: id => id.startsWith("@babel/runtime/"), 62 | input: "dist/index.mjs", 63 | output: { 64 | exports: "named", 65 | file: "dist/es5.js", 66 | format: "cjs", 67 | }, 68 | plugins: [ 69 | babel({ 70 | ...babelBaseConfig, 71 | babelHelpers: "runtime", 72 | plugins: ["@babel/transform-runtime"], 73 | }), 74 | ], 75 | }, 76 | { 77 | input: "dist/index.mjs", 78 | output: { 79 | exports: "named", 80 | file: "dist/umd.js", 81 | format: "umd", 82 | name: "EventTargetShim", 83 | }, 84 | plugins: [ 85 | terser(), 86 | babel({ 87 | ...babelBaseConfig, 88 | babelHelpers: "bundled", 89 | }), 90 | ], 91 | }, 92 | ] 93 | -------------------------------------------------------------------------------- /scripts/test.ts: -------------------------------------------------------------------------------- 1 | import chalk from "chalk" 2 | import crypto from "crypto" 3 | import fs from "fs" 4 | import { CoverageMap, createCoverageMap } from "istanbul-lib-coverage" 5 | import { createContext as createCoverageContext } from "istanbul-lib-report" 6 | import { createSourceMapStore } from "istanbul-lib-source-maps" 7 | import { create as createCoverageReporter } from "istanbul-reports" 8 | import os from "os" 9 | import path from "path" 10 | import playwright from "playwright" 11 | import rimraf from "rimraf" 12 | import url from "url" 13 | import util from "util" 14 | import webpackCallback, { Configuration, ProvidePlugin, Stats } from "webpack" 15 | 16 | const writeFile = util.promisify(fs.writeFile) 17 | const mkdir = util.promisify(fs.mkdir) 18 | const rmdir = util.promisify(rimraf) 19 | const webpack = util.promisify( 20 | webpackCallback, 21 | ) 22 | 23 | main(process.argv.slice(2)).catch(error => { 24 | process.exitCode = 1 25 | console.error(error) 26 | }) 27 | 28 | async function main(argv: readonly string[]) { 29 | const testOnNode = !argv.includes("--only-browsers") 30 | const testOnBrowsers = !argv.includes("--only-node") 31 | const workspacePath = path.join( 32 | os.tmpdir(), 33 | `event-target-shim-${crypto.randomBytes(4).toString("hex")}`, 34 | ) 35 | const coverageMap = createCoverageMap() 36 | 37 | await mkdir(workspacePath) 38 | try { 39 | await buildTests(workspacePath, testOnNode, testOnBrowsers) 40 | await runTests(workspacePath, coverageMap, testOnNode, testOnBrowsers) 41 | reportCoverage(coverageMap) 42 | } finally { 43 | await rmdir(workspacePath) 44 | } 45 | } 46 | 47 | async function buildTests( 48 | workspacePath: string, 49 | testOnNode: boolean, 50 | testOnBrowsers: boolean, 51 | ): Promise { 52 | console.log("======== Build Tests ".padEnd(80, "=")) 53 | 54 | if (testOnNode) { 55 | await build(workspacePath, false) 56 | } 57 | if (testOnBrowsers) { 58 | await writeFile( 59 | path.join(workspacePath, "playwright.html"), 60 | '\n', 61 | ) 62 | await build(workspacePath, true) 63 | } 64 | 65 | console.log("Done!") 66 | } 67 | 68 | async function runTests( 69 | workspacePath: string, 70 | coverageMap: CoverageMap, 71 | testOnNode: boolean, 72 | testOnBrowsers: boolean, 73 | ): Promise { 74 | console.log("======== Run Tests ".padEnd(80, "=")) 75 | 76 | let failures = 0 77 | 78 | if (testOnNode) { 79 | failures += await runTestsOnNode(workspacePath, coverageMap) 80 | } 81 | if (testOnBrowsers) { 82 | failures += await runTestsOnBrowsers(workspacePath, coverageMap) 83 | } 84 | 85 | console.log("-------- Result ".padEnd(80, "-")) 86 | if (failures) { 87 | console.log(chalk.bold.redBright("%d test cases failed."), failures) 88 | process.exitCode = 1 89 | } else { 90 | console.log(chalk.greenBright("All test cases succeeded ❤️")) 91 | } 92 | } 93 | 94 | async function runTestsOnNode( 95 | workspacePath: string, 96 | coverageMap: CoverageMap, 97 | ): Promise { 98 | console.log(chalk.magentaBright("-------- node ".padEnd(80, "-"))) 99 | 100 | await import(path.join(workspacePath, "node.js")) 101 | const { coverage, failures } = await (global as any).result 102 | 103 | await mergeCoverageMap(coverageMap, coverage) 104 | 105 | console.log() 106 | return failures 107 | } 108 | 109 | async function runTestsOnBrowsers( 110 | workspacePath: string, 111 | coverageMap: CoverageMap, 112 | ): Promise { 113 | let failures = 0 114 | for (const browserType of ["chromium", "firefox", "webkit"] as const) { 115 | console.log( 116 | chalk.magentaBright(`-------- ${browserType} `.padEnd(80, "-")), 117 | ) 118 | 119 | const browser = await playwright[browserType].launch() 120 | try { 121 | const context = await browser.newContext() 122 | const page = await context.newPage() 123 | 124 | // Redirect console logs. 125 | let consolePromise = Promise.resolve() 126 | page.on("console", msg => { 127 | consolePromise = consolePromise 128 | .then(() => Promise.all(msg.args().map(h => h.jsonValue()))) 129 | .then(args => console.log(...args)) 130 | }) 131 | 132 | // Run tests. 133 | await page.goto( 134 | url 135 | .pathToFileURL(path.join(workspacePath, "playwright.html")) 136 | .toString(), 137 | ) 138 | 139 | // Get result. 140 | const result = await page.evaluate("result") 141 | failures += result.failures 142 | await consolePromise 143 | 144 | // Merge coverage data. 145 | await mergeCoverageMap(coverageMap, result.coverage) 146 | } finally { 147 | await browser.close() 148 | } 149 | console.log() 150 | } 151 | 152 | return failures 153 | } 154 | 155 | async function build( 156 | workspacePath: string, 157 | forBrowsers: boolean, 158 | ): Promise { 159 | const conf: Configuration = { 160 | devtool: "inline-source-map", 161 | entry: path.resolve("test/fixtures/entrypoint.ts"), 162 | mode: "development", 163 | module: { 164 | rules: [ 165 | { 166 | test: /\.ts$/u, 167 | include: [path.resolve(__dirname, "../src")], 168 | loader: "babel-loader", 169 | options: { 170 | babelrc: false, 171 | plugins: ["istanbul"], 172 | sourceMaps: "inline", 173 | }, 174 | }, 175 | { 176 | test: /\.ts$/u, 177 | loader: "ts-loader", 178 | options: { 179 | configFile: path.resolve("tsconfig/test.json"), 180 | transpileOnly: true, 181 | }, 182 | }, 183 | ], 184 | }, 185 | output: { 186 | devtoolModuleFilenameTemplate: path.resolve("[resource-path]"), 187 | path: workspacePath, 188 | filename: "node.js", 189 | }, 190 | resolve: { 191 | extensions: [".ts", ".mjs", ".cjs", ".js", ".json"], 192 | }, 193 | target: "node", 194 | } 195 | 196 | if (forBrowsers) { 197 | conf.output!.filename = "playwright.js" 198 | conf.plugins = [ 199 | new ProvidePlugin({ 200 | Buffer: ["buffer/", "Buffer"], 201 | process: "process/browser", 202 | }), 203 | ] 204 | conf.resolve!.fallback = { 205 | assert: require.resolve("assert/"), 206 | buffer: require.resolve("buffer/"), 207 | fs: require.resolve("./empty.js"), 208 | path: require.resolve("path-browserify"), 209 | stream: require.resolve("stream-browserify"), 210 | url: require.resolve("url/"), 211 | util: require.resolve("util/"), 212 | } 213 | conf.target = "web" 214 | } 215 | 216 | const stats = await webpack(conf) 217 | if (stats?.hasErrors()) { 218 | throw new Error(stats.toString()) 219 | } 220 | } 221 | 222 | async function mergeCoverageMap( 223 | coverageMap: CoverageMap, 224 | rawData: any, 225 | ): Promise { 226 | const sourceMapStore = createSourceMapStore() 227 | const mappedData = toJSON( 228 | await sourceMapStore.transformCoverage(createCoverageMap(rawData)), 229 | ) 230 | 231 | const normalizedData = Object.entries(mappedData) 232 | .map(([k, v]) => [ 233 | path.normalize(k), 234 | { ...toJSON(v), path: path.normalize(k) }, 235 | ]) 236 | // eslint-disable-next-line no-sequences 237 | .reduce((obj, [k, v]) => ((obj[k] = v), obj), {}) 238 | 239 | try { 240 | coverageMap.merge(normalizedData) 241 | } catch (err) { 242 | console.log(normalizedData) 243 | throw err 244 | } 245 | } 246 | 247 | function reportCoverage(coverageMap: CoverageMap): void { 248 | const context = createCoverageContext({ coverageMap, dir: "coverage" }) 249 | 250 | // 出力する 251 | ;(createCoverageReporter("text-summary") as any).execute(context) 252 | ;(createCoverageReporter("lcov") as any).execute(context) 253 | console.log('See "coverage/lcov-report/index.html" for details.') 254 | console.log() 255 | } 256 | 257 | function toJSON(x: any): any { 258 | return typeof x.toJSON === "function" ? toJSON(x.toJSON()) : x 259 | } 260 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { setErrorHandler } from "./lib/error-handler" 2 | import { Event } from "./lib/event" 3 | import { 4 | getEventAttributeValue, 5 | setEventAttributeValue, 6 | } from "./lib/event-attribute-handler" 7 | import { EventTarget } from "./lib/event-target" 8 | import { defineCustomEventTarget, defineEventAttribute } from "./lib/legacy" 9 | import { setWarningHandler } from "./lib/warning-handler" 10 | 11 | export default EventTarget 12 | export { 13 | defineCustomEventTarget, 14 | defineEventAttribute, 15 | Event, 16 | EventTarget, 17 | getEventAttributeValue, 18 | setErrorHandler, 19 | setEventAttributeValue, 20 | setWarningHandler, 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/dom-exception.ts: -------------------------------------------------------------------------------- 1 | import { Global } from "./global" 2 | 3 | /** 4 | * Create a new InvalidStateError instance. 5 | * @param message The error message. 6 | */ 7 | export function createInvalidStateError(message: string): Error { 8 | if (Global.DOMException) { 9 | return new Global.DOMException(message, "InvalidStateError") 10 | } 11 | 12 | if (DOMException == null) { 13 | DOMException = class DOMException extends Error { 14 | constructor(msg: string) { 15 | super(msg) 16 | if ((Error as any).captureStackTrace) { 17 | ;(Error as any).captureStackTrace(this, DOMException) 18 | } 19 | } 20 | // eslint-disable-next-line class-methods-use-this 21 | get code() { 22 | return 11 23 | } 24 | // eslint-disable-next-line class-methods-use-this 25 | get name() { 26 | return "InvalidStateError" 27 | } 28 | } 29 | Object.defineProperties(DOMException.prototype, { 30 | code: { enumerable: true }, 31 | name: { enumerable: true }, 32 | }) 33 | defineErrorCodeProperties(DOMException) 34 | defineErrorCodeProperties(DOMException.prototype) 35 | } 36 | return new DOMException(message) 37 | } 38 | 39 | //------------------------------------------------------------------------------ 40 | // Helpers 41 | //------------------------------------------------------------------------------ 42 | 43 | let DOMException: { new (message: string): Error } | undefined 44 | 45 | const ErrorCodeMap = { 46 | INDEX_SIZE_ERR: 1, 47 | DOMSTRING_SIZE_ERR: 2, 48 | HIERARCHY_REQUEST_ERR: 3, 49 | WRONG_DOCUMENT_ERR: 4, 50 | INVALID_CHARACTER_ERR: 5, 51 | NO_DATA_ALLOWED_ERR: 6, 52 | NO_MODIFICATION_ALLOWED_ERR: 7, 53 | NOT_FOUND_ERR: 8, 54 | NOT_SUPPORTED_ERR: 9, 55 | INUSE_ATTRIBUTE_ERR: 10, 56 | INVALID_STATE_ERR: 11, 57 | SYNTAX_ERR: 12, 58 | INVALID_MODIFICATION_ERR: 13, 59 | NAMESPACE_ERR: 14, 60 | INVALID_ACCESS_ERR: 15, 61 | VALIDATION_ERR: 16, 62 | TYPE_MISMATCH_ERR: 17, 63 | SECURITY_ERR: 18, 64 | NETWORK_ERR: 19, 65 | ABORT_ERR: 20, 66 | URL_MISMATCH_ERR: 21, 67 | QUOTA_EXCEEDED_ERR: 22, 68 | TIMEOUT_ERR: 23, 69 | INVALID_NODE_TYPE_ERR: 24, 70 | DATA_CLONE_ERR: 25, 71 | } 72 | type ErrorCodeMap = typeof ErrorCodeMap 73 | 74 | function defineErrorCodeProperties(obj: any): void { 75 | const keys = Object.keys(ErrorCodeMap) as (keyof ErrorCodeMap)[] 76 | for (let i = 0; i < keys.length; ++i) { 77 | const key = keys[i] 78 | const value = ErrorCodeMap[key] 79 | Object.defineProperty(obj, key, { 80 | get() { 81 | return value 82 | }, 83 | configurable: true, 84 | enumerable: true, 85 | }) 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/error-handler.ts: -------------------------------------------------------------------------------- 1 | import { anyToString, assertType } from "./misc" 2 | 3 | declare const console: any 4 | declare const dispatchEvent: any 5 | declare const ErrorEvent: any 6 | declare const process: any 7 | 8 | let currentErrorHandler: setErrorHandler.ErrorHandler | undefined 9 | 10 | /** 11 | * Set the error handler. 12 | * @param value The error handler to set. 13 | */ 14 | export function setErrorHandler( 15 | value: setErrorHandler.ErrorHandler | undefined, 16 | ): void { 17 | assertType( 18 | typeof value === "function" || value === undefined, 19 | "The error handler must be a function or undefined, but got %o.", 20 | value, 21 | ) 22 | currentErrorHandler = value 23 | } 24 | export namespace setErrorHandler { 25 | /** 26 | * The error handler. 27 | * @param error The thrown error object. 28 | */ 29 | export type ErrorHandler = (error: Error) => void 30 | } 31 | 32 | /** 33 | * Print a error message. 34 | * @param maybeError The error object. 35 | */ 36 | export function reportError(maybeError: unknown): void { 37 | try { 38 | const error = 39 | maybeError instanceof Error 40 | ? maybeError 41 | : new Error(anyToString(maybeError)) 42 | 43 | // Call the user-defined error handler if exists. 44 | if (currentErrorHandler) { 45 | currentErrorHandler(error) 46 | return 47 | } 48 | 49 | // Dispatch an `error` event if this is on a browser. 50 | if ( 51 | typeof dispatchEvent === "function" && 52 | typeof ErrorEvent === "function" 53 | ) { 54 | dispatchEvent( 55 | new ErrorEvent("error", { error, message: error.message }), 56 | ) 57 | } 58 | 59 | // Emit an `uncaughtException` event if this is on Node.js. 60 | //istanbul ignore else 61 | else if ( 62 | typeof process !== "undefined" && 63 | typeof process.emit === "function" 64 | ) { 65 | process.emit("uncaughtException", error) 66 | return 67 | } 68 | 69 | // Otherwise, print the error. 70 | console.error(error) 71 | } catch { 72 | // ignore. 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/lib/event-attribute-handler.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "./event" 2 | import { EventTarget, getEventTargetInternalData } from "./event-target" 3 | import { addListener, ListenerList, removeListener } from "./listener-list" 4 | import { ensureListenerList } from "./listener-list-map" 5 | import { InvalidAttributeHandler } from "./warnings" 6 | 7 | /** 8 | * Get the current value of a given event attribute. 9 | * @param target The `EventTarget` object to get. 10 | * @param type The event type. 11 | */ 12 | export function getEventAttributeValue< 13 | TEventTarget extends EventTarget, 14 | TEvent extends Event 15 | >( 16 | target: TEventTarget, 17 | type: string, 18 | ): EventTarget.CallbackFunction | null { 19 | const listMap = getEventTargetInternalData(target, "target") 20 | return listMap[type]?.attrCallback ?? null 21 | } 22 | 23 | /** 24 | * Set an event listener to a given event attribute. 25 | * @param target The `EventTarget` object to set. 26 | * @param type The event type. 27 | * @param callback The event listener. 28 | */ 29 | export function setEventAttributeValue( 30 | target: EventTarget, 31 | type: string, 32 | callback: EventTarget.CallbackFunction | null, 33 | ): void { 34 | if (callback != null && typeof callback !== "function") { 35 | InvalidAttributeHandler.warn(callback) 36 | } 37 | 38 | if ( 39 | typeof callback === "function" || 40 | (typeof callback === "object" && callback !== null) 41 | ) { 42 | upsertEventAttributeListener(target, type, callback) 43 | } else { 44 | removeEventAttributeListener(target, type) 45 | } 46 | } 47 | 48 | //------------------------------------------------------------------------------ 49 | // Helpers 50 | //------------------------------------------------------------------------------ 51 | 52 | /** 53 | * Update or insert the given event attribute handler. 54 | * @param target The `EventTarget` object to set. 55 | * @param type The event type. 56 | * @param callback The event listener. 57 | */ 58 | function upsertEventAttributeListener< 59 | TEventTarget extends EventTarget 60 | >( 61 | target: TEventTarget, 62 | type: string, 63 | callback: EventTarget.CallbackFunction, 64 | ): void { 65 | const list = ensureListenerList( 66 | getEventTargetInternalData(target, "target"), 67 | String(type), 68 | ) 69 | list.attrCallback = callback 70 | 71 | if (list.attrListener == null) { 72 | list.attrListener = addListener( 73 | list, 74 | defineEventAttributeCallback(list), 75 | false, 76 | false, 77 | false, 78 | undefined, 79 | ) 80 | } 81 | } 82 | 83 | /** 84 | * Remove the given event attribute handler. 85 | * @param target The `EventTarget` object to remove. 86 | * @param type The event type. 87 | * @param callback The event listener. 88 | */ 89 | function removeEventAttributeListener( 90 | target: EventTarget, 91 | type: string, 92 | ): void { 93 | const listMap = getEventTargetInternalData(target, "target") 94 | const list = listMap[String(type)] 95 | if (list && list.attrListener) { 96 | removeListener(list, list.attrListener.callback, false) 97 | list.attrCallback = list.attrListener = undefined 98 | } 99 | } 100 | 101 | /** 102 | * Define the callback function for the given listener list object. 103 | * It calls `attrCallback` property if the property value is a function. 104 | * @param list The `ListenerList` object. 105 | */ 106 | function defineEventAttributeCallback( 107 | list: ListenerList, 108 | ): EventTarget.CallbackFunction { 109 | return function (event) { 110 | const callback = list.attrCallback 111 | if (typeof callback === "function") { 112 | callback.call(this, event) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/lib/event-target.ts: -------------------------------------------------------------------------------- 1 | import { createInvalidStateError } from "./dom-exception" 2 | import { Event, getEventInternalData } from "./event" 3 | import { EventWrapper } from "./event-wrapper" 4 | import { Global } from "./global" 5 | import { 6 | invokeCallback, 7 | isCapture, 8 | isOnce, 9 | isPassive, 10 | isRemoved, 11 | Listener, 12 | } from "./listener" 13 | import { 14 | addListener, 15 | findIndexOfListener, 16 | removeListener, 17 | removeListenerAt, 18 | } from "./listener-list" 19 | import { 20 | createListenerListMap, 21 | ensureListenerList, 22 | ListenerListMap, 23 | } from "./listener-list-map" 24 | import { assertType, format } from "./misc" 25 | import { 26 | EventListenerWasDuplicated, 27 | InvalidEventListener, 28 | OptionWasIgnored, 29 | } from "./warnings" 30 | 31 | /** 32 | * An implementation of the `EventTarget` interface. 33 | * @see https://dom.spec.whatwg.org/#eventtarget 34 | */ 35 | export class EventTarget< 36 | TEventMap extends Record = Record, 37 | TMode extends "standard" | "strict" = "standard" 38 | > { 39 | /** 40 | * Initialize this instance. 41 | */ 42 | constructor() { 43 | internalDataMap.set(this, createListenerListMap()) 44 | } 45 | 46 | /** 47 | * Add an event listener. 48 | * @param type The event type. 49 | * @param callback The event listener. 50 | * @param options Options. 51 | */ 52 | addEventListener( 53 | type: T, 54 | callback?: EventTarget.EventListener | null, 55 | options?: EventTarget.AddOptions, 56 | ): void 57 | 58 | /** 59 | * Add an event listener. 60 | * @param type The event type. 61 | * @param callback The event listener. 62 | * @param options Options. 63 | */ 64 | addEventListener( 65 | type: string, 66 | callback?: EventTarget.FallbackEventListener, 67 | options?: EventTarget.AddOptions, 68 | ): void 69 | 70 | /** 71 | * Add an event listener. 72 | * @param type The event type. 73 | * @param callback The event listener. 74 | * @param capture The capture flag. 75 | * @deprecated Use `{capture: boolean}` object instead of a boolean value. 76 | */ 77 | addEventListener( 78 | type: T, 79 | callback: 80 | | EventTarget.EventListener 81 | | null 82 | | undefined, 83 | capture: boolean, 84 | ): void 85 | 86 | /** 87 | * Add an event listener. 88 | * @param type The event type. 89 | * @param callback The event listener. 90 | * @param capture The capture flag. 91 | * @deprecated Use `{capture: boolean}` object instead of a boolean value. 92 | */ 93 | addEventListener( 94 | type: string, 95 | callback: EventTarget.FallbackEventListener, 96 | capture: boolean, 97 | ): void 98 | 99 | // Implementation 100 | addEventListener( 101 | type0: T, 102 | callback0?: EventTarget.EventListener | null, 103 | options0?: boolean | EventTarget.AddOptions, 104 | ): void { 105 | const listenerMap = $(this) 106 | const { 107 | callback, 108 | capture, 109 | once, 110 | passive, 111 | signal, 112 | type, 113 | } = normalizeAddOptions(type0, callback0, options0) 114 | if (callback == null || signal?.aborted) { 115 | return 116 | } 117 | const list = ensureListenerList(listenerMap, type) 118 | 119 | // Find existing listener. 120 | const i = findIndexOfListener(list, callback, capture) 121 | if (i !== -1) { 122 | warnDuplicate(list.listeners[i], passive, once, signal) 123 | return 124 | } 125 | 126 | // Add the new listener. 127 | addListener(list, callback, capture, passive, once, signal) 128 | } 129 | 130 | /** 131 | * Remove an added event listener. 132 | * @param type The event type. 133 | * @param callback The event listener. 134 | * @param options Options. 135 | */ 136 | removeEventListener( 137 | type: T, 138 | callback?: EventTarget.EventListener | null, 139 | options?: EventTarget.Options, 140 | ): void 141 | 142 | /** 143 | * Remove an added event listener. 144 | * @param type The event type. 145 | * @param callback The event listener. 146 | * @param options Options. 147 | */ 148 | removeEventListener( 149 | type: string, 150 | callback?: EventTarget.FallbackEventListener, 151 | options?: EventTarget.Options, 152 | ): void 153 | 154 | /** 155 | * Remove an added event listener. 156 | * @param type The event type. 157 | * @param callback The event listener. 158 | * @param capture The capture flag. 159 | * @deprecated Use `{capture: boolean}` object instead of a boolean value. 160 | */ 161 | removeEventListener( 162 | type: T, 163 | callback: 164 | | EventTarget.EventListener 165 | | null 166 | | undefined, 167 | capture: boolean, 168 | ): void 169 | 170 | /** 171 | * Remove an added event listener. 172 | * @param type The event type. 173 | * @param callback The event listener. 174 | * @param capture The capture flag. 175 | * @deprecated Use `{capture: boolean}` object instead of a boolean value. 176 | */ 177 | removeEventListener( 178 | type: string, 179 | callback: EventTarget.FallbackEventListener, 180 | capture: boolean, 181 | ): void 182 | 183 | // Implementation 184 | removeEventListener( 185 | type0: T, 186 | callback0?: EventTarget.EventListener | null, 187 | options0?: boolean | EventTarget.Options, 188 | ): void { 189 | const listenerMap = $(this) 190 | const { callback, capture, type } = normalizeOptions( 191 | type0, 192 | callback0, 193 | options0, 194 | ) 195 | const list = listenerMap[type] 196 | 197 | if (callback != null && list) { 198 | removeListener(list, callback, capture) 199 | } 200 | } 201 | 202 | /** 203 | * Dispatch an event. 204 | * @param event The `Event` object to dispatch. 205 | */ 206 | dispatchEvent( 207 | event: EventTarget.EventData, 208 | ): boolean 209 | 210 | /** 211 | * Dispatch an event. 212 | * @param event The `Event` object to dispatch. 213 | */ 214 | dispatchEvent(event: EventTarget.FallbackEvent): boolean 215 | 216 | // Implementation 217 | dispatchEvent( 218 | e: 219 | | EventTarget.EventData 220 | | EventTarget.FallbackEvent, 221 | ): boolean { 222 | const list = $(this)[String(e.type)] 223 | if (list == null) { 224 | return true 225 | } 226 | 227 | const event = e instanceof Event ? e : EventWrapper.wrap(e) 228 | const eventData = getEventInternalData(event, "event") 229 | if (eventData.dispatchFlag) { 230 | throw createInvalidStateError("This event has been in dispatching.") 231 | } 232 | 233 | eventData.dispatchFlag = true 234 | eventData.target = eventData.currentTarget = this 235 | 236 | if (!eventData.stopPropagationFlag) { 237 | const { cow, listeners } = list 238 | 239 | // Set copy-on-write flag. 240 | list.cow = true 241 | 242 | // Call listeners. 243 | for (let i = 0; i < listeners.length; ++i) { 244 | const listener = listeners[i] 245 | 246 | // Skip if removed. 247 | if (isRemoved(listener)) { 248 | continue 249 | } 250 | 251 | // Remove this listener if has the `once` flag. 252 | if (isOnce(listener) && removeListenerAt(list, i, !cow)) { 253 | // Because this listener was removed, the next index is the 254 | // same as the current value. 255 | i -= 1 256 | } 257 | 258 | // Call this listener with the `passive` flag. 259 | eventData.inPassiveListenerFlag = isPassive(listener) 260 | invokeCallback(listener, this, event) 261 | eventData.inPassiveListenerFlag = false 262 | 263 | // Stop if the `event.stopImmediatePropagation()` method was called. 264 | if (eventData.stopImmediatePropagationFlag) { 265 | break 266 | } 267 | } 268 | 269 | // Restore copy-on-write flag. 270 | if (!cow) { 271 | list.cow = false 272 | } 273 | } 274 | 275 | eventData.target = null 276 | eventData.currentTarget = null 277 | eventData.stopImmediatePropagationFlag = false 278 | eventData.stopPropagationFlag = false 279 | eventData.dispatchFlag = false 280 | 281 | return !eventData.canceledFlag 282 | } 283 | } 284 | 285 | export namespace EventTarget { 286 | /** 287 | * The event listener. 288 | */ 289 | export type EventListener< 290 | TEventTarget extends EventTarget, 291 | TEvent extends Event 292 | > = CallbackFunction | CallbackObject 293 | 294 | /** 295 | * The event listener function. 296 | */ 297 | export interface CallbackFunction< 298 | TEventTarget extends EventTarget, 299 | TEvent extends Event 300 | > { 301 | (this: TEventTarget, event: TEvent): void 302 | } 303 | 304 | /** 305 | * The event listener object. 306 | * @see https://dom.spec.whatwg.org/#callbackdef-eventlistener 307 | */ 308 | export interface CallbackObject { 309 | handleEvent(event: TEvent): void 310 | } 311 | 312 | /** 313 | * The common options for both `addEventListener` and `removeEventListener` methods. 314 | * @see https://dom.spec.whatwg.org/#dictdef-eventlisteneroptions 315 | */ 316 | export interface Options { 317 | capture?: boolean 318 | } 319 | 320 | /** 321 | * The options for the `addEventListener` methods. 322 | * @see https://dom.spec.whatwg.org/#dictdef-addeventlisteneroptions 323 | */ 324 | export interface AddOptions extends Options { 325 | passive?: boolean 326 | once?: boolean 327 | signal?: AbortSignal | null | undefined 328 | } 329 | 330 | /** 331 | * The abort signal. 332 | * @see https://dom.spec.whatwg.org/#abortsignal 333 | */ 334 | export interface AbortSignal extends EventTarget<{ abort: Event }> { 335 | readonly aborted: boolean 336 | onabort: CallbackFunction | null 337 | } 338 | 339 | /** 340 | * The event data to dispatch in strict mode. 341 | */ 342 | export type EventData< 343 | TEventMap extends Record, 344 | TMode extends "standard" | "strict", 345 | TEventType extends string 346 | > = TMode extends "strict" 347 | ? IsValidEventMap extends true 348 | ? ExplicitType & 349 | Omit & 350 | Partial> 351 | : never 352 | : never 353 | 354 | /** 355 | * Define explicit `type` property if `T` is a string literal. 356 | * Otherwise, never. 357 | */ 358 | export type ExplicitType = string extends T 359 | ? never 360 | : { readonly type: T } 361 | 362 | /** 363 | * The event listener type in standard mode. 364 | * Otherwise, never. 365 | */ 366 | export type FallbackEventListener< 367 | TEventTarget extends EventTarget, 368 | TMode extends "standard" | "strict" 369 | > = TMode extends "standard" 370 | ? EventListener | null | undefined 371 | : never 372 | 373 | /** 374 | * The event type in standard mode. 375 | * Otherwise, never. 376 | */ 377 | export type FallbackEvent< 378 | TMode extends "standard" | "strict" 379 | > = TMode extends "standard" ? Event : never 380 | 381 | /** 382 | * Check if given event map is valid. 383 | * It's valid if the keys of the event map are narrower than `string`. 384 | */ 385 | export type IsValidEventMap = string extends keyof T ? false : true 386 | } 387 | 388 | export { $ as getEventTargetInternalData } 389 | 390 | //------------------------------------------------------------------------------ 391 | // Helpers 392 | //------------------------------------------------------------------------------ 393 | 394 | /** 395 | * Internal data for EventTarget 396 | */ 397 | type EventTargetInternalData = ListenerListMap 398 | 399 | /** 400 | * Internal data. 401 | */ 402 | const internalDataMap = new WeakMap() 403 | 404 | /** 405 | * Get private data. 406 | * @param target The event target object to get private data. 407 | * @param name The variable name to report. 408 | * @returns The private data of the event. 409 | */ 410 | function $(target: any, name = "this"): EventTargetInternalData { 411 | const retv = internalDataMap.get(target) 412 | assertType( 413 | retv != null, 414 | "'%s' must be an object that EventTarget constructor created, but got another one: %o", 415 | name, 416 | target, 417 | ) 418 | return retv 419 | } 420 | 421 | /** 422 | * Normalize options. 423 | * @param options The options to normalize. 424 | */ 425 | function normalizeAddOptions( 426 | type: string, 427 | callback: EventTarget.EventListener | null | undefined, 428 | options: boolean | EventTarget.AddOptions | undefined, 429 | ): { 430 | type: string 431 | callback: EventTarget.EventListener | undefined 432 | capture: boolean 433 | passive: boolean 434 | once: boolean 435 | signal: EventTarget.AbortSignal | undefined 436 | } { 437 | assertCallback(callback) 438 | 439 | if (typeof options === "object" && options !== null) { 440 | return { 441 | type: String(type), 442 | callback: callback ?? undefined, 443 | capture: Boolean(options.capture), 444 | passive: Boolean(options.passive), 445 | once: Boolean(options.once), 446 | signal: options.signal ?? undefined, 447 | } 448 | } 449 | 450 | return { 451 | type: String(type), 452 | callback: callback ?? undefined, 453 | capture: Boolean(options), 454 | passive: false, 455 | once: false, 456 | signal: undefined, 457 | } 458 | } 459 | 460 | /** 461 | * Normalize options. 462 | * @param options The options to normalize. 463 | */ 464 | function normalizeOptions( 465 | type: string, 466 | callback: EventTarget.EventListener | null | undefined, 467 | options: boolean | EventTarget.Options | undefined, 468 | ): { 469 | type: string 470 | callback: EventTarget.EventListener | undefined 471 | capture: boolean 472 | } { 473 | assertCallback(callback) 474 | 475 | if (typeof options === "object" && options !== null) { 476 | return { 477 | type: String(type), 478 | callback: callback ?? undefined, 479 | capture: Boolean(options.capture), 480 | } 481 | } 482 | 483 | return { 484 | type: String(type), 485 | callback: callback ?? undefined, 486 | capture: Boolean(options), 487 | } 488 | } 489 | 490 | /** 491 | * Assert the type of 'callback' argument. 492 | * @param callback The callback to check. 493 | */ 494 | function assertCallback(callback: any): void { 495 | if ( 496 | typeof callback === "function" || 497 | (typeof callback === "object" && 498 | callback !== null && 499 | typeof callback.handleEvent === "function") 500 | ) { 501 | return 502 | } 503 | if (callback == null || typeof callback === "object") { 504 | InvalidEventListener.warn(callback) 505 | return 506 | } 507 | 508 | throw new TypeError(format(InvalidEventListener.message, [callback])) 509 | } 510 | 511 | /** 512 | * Print warning for duplicated. 513 | * @param listener The current listener that is duplicated. 514 | * @param passive The passive flag of the new duplicated listener. 515 | * @param once The once flag of the new duplicated listener. 516 | * @param signal The signal object of the new duplicated listener. 517 | */ 518 | function warnDuplicate( 519 | listener: Listener, 520 | passive: boolean, 521 | once: boolean, 522 | signal: EventTarget.AbortSignal | undefined, 523 | ): void { 524 | EventListenerWasDuplicated.warn( 525 | isCapture(listener) ? "capture" : "bubble", 526 | listener.callback, 527 | ) 528 | 529 | if (isPassive(listener) !== passive) { 530 | OptionWasIgnored.warn("passive") 531 | } 532 | if (isOnce(listener) !== once) { 533 | OptionWasIgnored.warn("once") 534 | } 535 | if (listener.signal !== signal) { 536 | OptionWasIgnored.warn("signal") 537 | } 538 | } 539 | 540 | // Set enumerable 541 | const keys = Object.getOwnPropertyNames(EventTarget.prototype) 542 | for (let i = 0; i < keys.length; ++i) { 543 | if (keys[i] === "constructor") { 544 | continue 545 | } 546 | Object.defineProperty(EventTarget.prototype, keys[i], { enumerable: true }) 547 | } 548 | 549 | // Ensure `eventTarget instanceof window.EventTarget` is `true`. 550 | if ( 551 | typeof Global !== "undefined" && 552 | typeof Global.EventTarget !== "undefined" 553 | ) { 554 | Object.setPrototypeOf(EventTarget.prototype, Global.EventTarget.prototype) 555 | } 556 | -------------------------------------------------------------------------------- /src/lib/event-wrapper.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "./event" 2 | import { Global } from "./global" 3 | import { assertType } from "./misc" 4 | 5 | /** 6 | * An implementation of `Event` interface, that wraps a given event object. 7 | * This class controls the internal state of `Event`. 8 | * @see https://dom.spec.whatwg.org/#interface-event 9 | */ 10 | export class EventWrapper extends Event { 11 | /** 12 | * Wrap a given event object to control states. 13 | * @param event The event-like object to wrap. 14 | */ 15 | static wrap(event: T): EventWrapperOf { 16 | return new (getWrapperClassOf(event))(event) 17 | } 18 | 19 | protected constructor(event: Event) { 20 | super(event.type, { 21 | bubbles: event.bubbles, 22 | cancelable: event.cancelable, 23 | composed: event.composed, 24 | }) 25 | 26 | if (event.cancelBubble) { 27 | super.stopPropagation() 28 | } 29 | if (event.defaultPrevented) { 30 | super.preventDefault() 31 | } 32 | 33 | internalDataMap.set(this, { original: event }) 34 | 35 | // Define accessors 36 | const keys = Object.keys(event) 37 | for (let i = 0; i < keys.length; ++i) { 38 | const key = keys[i] 39 | if (!(key in this)) { 40 | Object.defineProperty( 41 | this, 42 | key, 43 | defineRedirectDescriptor(event, key), 44 | ) 45 | } 46 | } 47 | } 48 | 49 | stopPropagation(): void { 50 | super.stopPropagation() 51 | 52 | const { original } = $(this) 53 | if ("stopPropagation" in original) { 54 | original.stopPropagation!() 55 | } 56 | } 57 | 58 | get cancelBubble(): boolean { 59 | return super.cancelBubble 60 | } 61 | set cancelBubble(value: boolean) { 62 | super.cancelBubble = value 63 | 64 | const { original } = $(this) 65 | if ("cancelBubble" in original) { 66 | original.cancelBubble = value 67 | } 68 | } 69 | 70 | stopImmediatePropagation(): void { 71 | super.stopImmediatePropagation() 72 | 73 | const { original } = $(this) 74 | if ("stopImmediatePropagation" in original) { 75 | original.stopImmediatePropagation!() 76 | } 77 | } 78 | 79 | get returnValue(): boolean { 80 | return super.returnValue 81 | } 82 | set returnValue(value: boolean) { 83 | super.returnValue = value 84 | 85 | const { original } = $(this) 86 | if ("returnValue" in original) { 87 | original.returnValue = value 88 | } 89 | } 90 | 91 | preventDefault(): void { 92 | super.preventDefault() 93 | 94 | const { original } = $(this) 95 | if ("preventDefault" in original) { 96 | original.preventDefault!() 97 | } 98 | } 99 | 100 | get timeStamp(): number { 101 | const { original } = $(this) 102 | if ("timeStamp" in original) { 103 | return original.timeStamp! 104 | } 105 | return super.timeStamp 106 | } 107 | } 108 | 109 | //------------------------------------------------------------------------------ 110 | // Helpers 111 | //------------------------------------------------------------------------------ 112 | 113 | type EventLike = { readonly type: string } & Partial 114 | type EventWrapperOf = Event & 115 | Omit 116 | 117 | interface EventWrapperInternalData { 118 | readonly original: EventLike 119 | } 120 | 121 | /** 122 | * Private data for event wrappers. 123 | */ 124 | const internalDataMap = new WeakMap() 125 | 126 | /** 127 | * Get private data. 128 | * @param event The event object to get private data. 129 | * @returns The private data of the event. 130 | */ 131 | function $(event: unknown): EventWrapperInternalData { 132 | const retv = internalDataMap.get(event) 133 | assertType( 134 | retv != null, 135 | "'this' is expected an Event object, but got", 136 | event, 137 | ) 138 | return retv 139 | } 140 | 141 | /** 142 | * Cache for wrapper classes. 143 | * @type {WeakMap} 144 | * @private 145 | */ 146 | const wrapperClassCache = new WeakMap() 147 | 148 | // Make association for wrappers. 149 | wrapperClassCache.set(Object.prototype, EventWrapper) 150 | if (typeof Global !== "undefined" && typeof Global.Event !== "undefined") { 151 | wrapperClassCache.set(Global.Event.prototype, EventWrapper) 152 | } 153 | 154 | /** 155 | * Get the wrapper class of a given prototype. 156 | * @param originalEvent The event object to wrap. 157 | */ 158 | function getWrapperClassOf( 159 | originalEvent: T, 160 | ): { new (e: T): EventWrapperOf } { 161 | const prototype = Object.getPrototypeOf(originalEvent) 162 | if (prototype == null) { 163 | return EventWrapper as any 164 | } 165 | 166 | let wrapper: any = wrapperClassCache.get(prototype) 167 | if (wrapper == null) { 168 | wrapper = defineWrapper(getWrapperClassOf(prototype), prototype) 169 | wrapperClassCache.set(prototype, wrapper) 170 | } 171 | 172 | return wrapper 173 | } 174 | 175 | /** 176 | * Define new wrapper class. 177 | * @param BaseEventWrapper The base wrapper class. 178 | * @param originalPrototype The prototype of the original event. 179 | */ 180 | function defineWrapper(BaseEventWrapper: any, originalPrototype: any): any { 181 | class CustomEventWrapper extends BaseEventWrapper {} 182 | 183 | const keys = Object.keys(originalPrototype) 184 | for (let i = 0; i < keys.length; ++i) { 185 | Object.defineProperty( 186 | CustomEventWrapper.prototype, 187 | keys[i], 188 | defineRedirectDescriptor(originalPrototype, keys[i]), 189 | ) 190 | } 191 | 192 | return CustomEventWrapper 193 | } 194 | 195 | /** 196 | * Get the property descriptor to redirect a given property. 197 | */ 198 | function defineRedirectDescriptor(obj: any, key: string): PropertyDescriptor { 199 | const d = Object.getOwnPropertyDescriptor(obj, key)! 200 | return { 201 | get() { 202 | const original: any = $(this).original 203 | const value = original[key] 204 | if (typeof value === "function") { 205 | return value.bind(original) 206 | } 207 | return value 208 | }, 209 | set(value: any) { 210 | const original: any = $(this).original 211 | original[key] = value 212 | }, 213 | configurable: d.configurable, 214 | enumerable: d.enumerable, 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/lib/event.ts: -------------------------------------------------------------------------------- 1 | import { EventTarget } from "./event-target" // Used as only type, so no circular. 2 | import { Global } from "./global" 3 | import { assertType } from "./misc" 4 | import { 5 | CanceledInPassiveListener, 6 | FalsyWasAssignedToCancelBubble, 7 | InitEventWasCalledWhileDispatching, 8 | NonCancelableEventWasCanceled, 9 | TruthyWasAssignedToReturnValue, 10 | } from "./warnings" 11 | 12 | /*eslint-disable class-methods-use-this */ 13 | 14 | /** 15 | * An implementation of `Event` interface, that wraps a given event object. 16 | * `EventTarget` shim can control the internal state of this `Event` objects. 17 | * @see https://dom.spec.whatwg.org/#event 18 | */ 19 | export class Event { 20 | /** 21 | * @see https://dom.spec.whatwg.org/#dom-event-none 22 | */ 23 | static get NONE(): number { 24 | return NONE 25 | } 26 | 27 | /** 28 | * @see https://dom.spec.whatwg.org/#dom-event-capturing_phase 29 | */ 30 | static get CAPTURING_PHASE(): number { 31 | return CAPTURING_PHASE 32 | } 33 | 34 | /** 35 | * @see https://dom.spec.whatwg.org/#dom-event-at_target 36 | */ 37 | static get AT_TARGET(): number { 38 | return AT_TARGET 39 | } 40 | 41 | /** 42 | * @see https://dom.spec.whatwg.org/#dom-event-bubbling_phase 43 | */ 44 | static get BUBBLING_PHASE(): number { 45 | return BUBBLING_PHASE 46 | } 47 | 48 | /** 49 | * Initialize this event instance. 50 | * @param type The type of this event. 51 | * @param eventInitDict Options to initialize. 52 | * @see https://dom.spec.whatwg.org/#dom-event-event 53 | */ 54 | constructor(type: TEventType, eventInitDict?: Event.EventInit) { 55 | Object.defineProperty(this, "isTrusted", { 56 | value: false, 57 | enumerable: true, 58 | }) 59 | 60 | const opts = eventInitDict ?? {} 61 | internalDataMap.set(this, { 62 | type: String(type), 63 | bubbles: Boolean(opts.bubbles), 64 | cancelable: Boolean(opts.cancelable), 65 | composed: Boolean(opts.composed), 66 | target: null, 67 | currentTarget: null, 68 | stopPropagationFlag: false, 69 | stopImmediatePropagationFlag: false, 70 | canceledFlag: false, 71 | inPassiveListenerFlag: false, 72 | dispatchFlag: false, 73 | timeStamp: Date.now(), 74 | }) 75 | } 76 | 77 | /** 78 | * The type of this event. 79 | * @see https://dom.spec.whatwg.org/#dom-event-type 80 | */ 81 | get type(): TEventType { 82 | return $(this).type as TEventType 83 | } 84 | 85 | /** 86 | * The event target of the current dispatching. 87 | * @see https://dom.spec.whatwg.org/#dom-event-target 88 | */ 89 | get target(): EventTarget | null { 90 | return $(this).target 91 | } 92 | 93 | /** 94 | * The event target of the current dispatching. 95 | * @deprecated Use the `target` property instead. 96 | * @see https://dom.spec.whatwg.org/#dom-event-srcelement 97 | */ 98 | get srcElement(): EventTarget | null { 99 | return $(this).target 100 | } 101 | 102 | /** 103 | * The event target of the current dispatching. 104 | * @see https://dom.spec.whatwg.org/#dom-event-currenttarget 105 | */ 106 | get currentTarget(): EventTarget | null { 107 | return $(this).currentTarget 108 | } 109 | 110 | /** 111 | * The event target of the current dispatching. 112 | * This doesn't support node tree. 113 | * @see https://dom.spec.whatwg.org/#dom-event-composedpath 114 | */ 115 | composedPath(): EventTarget[] { 116 | const currentTarget = $(this).currentTarget 117 | if (currentTarget) { 118 | return [currentTarget] 119 | } 120 | return [] 121 | } 122 | 123 | /** 124 | * @see https://dom.spec.whatwg.org/#dom-event-none 125 | */ 126 | get NONE(): number { 127 | return NONE 128 | } 129 | 130 | /** 131 | * @see https://dom.spec.whatwg.org/#dom-event-capturing_phase 132 | */ 133 | get CAPTURING_PHASE(): number { 134 | return CAPTURING_PHASE 135 | } 136 | 137 | /** 138 | * @see https://dom.spec.whatwg.org/#dom-event-at_target 139 | */ 140 | get AT_TARGET(): number { 141 | return AT_TARGET 142 | } 143 | 144 | /** 145 | * @see https://dom.spec.whatwg.org/#dom-event-bubbling_phase 146 | */ 147 | get BUBBLING_PHASE(): number { 148 | return BUBBLING_PHASE 149 | } 150 | 151 | /** 152 | * The current event phase. 153 | * @see https://dom.spec.whatwg.org/#dom-event-eventphase 154 | */ 155 | get eventPhase(): number { 156 | return $(this).dispatchFlag ? 2 : 0 157 | } 158 | 159 | /** 160 | * Stop event bubbling. 161 | * Because this shim doesn't support node tree, this merely changes the `cancelBubble` property value. 162 | * @see https://dom.spec.whatwg.org/#dom-event-stoppropagation 163 | */ 164 | stopPropagation(): void { 165 | $(this).stopPropagationFlag = true 166 | } 167 | 168 | /** 169 | * `true` if event bubbling was stopped. 170 | * @deprecated 171 | * @see https://dom.spec.whatwg.org/#dom-event-cancelbubble 172 | */ 173 | get cancelBubble(): boolean { 174 | return $(this).stopPropagationFlag 175 | } 176 | 177 | /** 178 | * Stop event bubbling if `true` is set. 179 | * @deprecated Use the `stopPropagation()` method instead. 180 | * @see https://dom.spec.whatwg.org/#dom-event-cancelbubble 181 | */ 182 | set cancelBubble(value: boolean) { 183 | if (value) { 184 | $(this).stopPropagationFlag = true 185 | } else { 186 | FalsyWasAssignedToCancelBubble.warn() 187 | } 188 | } 189 | 190 | /** 191 | * Stop event bubbling and subsequent event listener callings. 192 | * @see https://dom.spec.whatwg.org/#dom-event-stopimmediatepropagation 193 | */ 194 | stopImmediatePropagation(): void { 195 | const data = $(this) 196 | data.stopPropagationFlag = data.stopImmediatePropagationFlag = true 197 | } 198 | 199 | /** 200 | * `true` if this event will bubble. 201 | * @see https://dom.spec.whatwg.org/#dom-event-bubbles 202 | */ 203 | get bubbles(): boolean { 204 | return $(this).bubbles 205 | } 206 | 207 | /** 208 | * `true` if this event can be canceled by the `preventDefault()` method. 209 | * @see https://dom.spec.whatwg.org/#dom-event-cancelable 210 | */ 211 | get cancelable(): boolean { 212 | return $(this).cancelable 213 | } 214 | 215 | /** 216 | * `true` if the default behavior will act. 217 | * @deprecated Use the `defaultPrevented` proeprty instead. 218 | * @see https://dom.spec.whatwg.org/#dom-event-returnvalue 219 | */ 220 | get returnValue(): boolean { 221 | return !$(this).canceledFlag 222 | } 223 | 224 | /** 225 | * Cancel the default behavior if `false` is set. 226 | * @deprecated Use the `preventDefault()` method instead. 227 | * @see https://dom.spec.whatwg.org/#dom-event-returnvalue 228 | */ 229 | set returnValue(value: boolean) { 230 | if (!value) { 231 | setCancelFlag($(this)) 232 | } else { 233 | TruthyWasAssignedToReturnValue.warn() 234 | } 235 | } 236 | 237 | /** 238 | * Cancel the default behavior. 239 | * @see https://dom.spec.whatwg.org/#dom-event-preventdefault 240 | */ 241 | preventDefault(): void { 242 | setCancelFlag($(this)) 243 | } 244 | 245 | /** 246 | * `true` if the default behavior was canceled. 247 | * @see https://dom.spec.whatwg.org/#dom-event-defaultprevented 248 | */ 249 | get defaultPrevented(): boolean { 250 | return $(this).canceledFlag 251 | } 252 | 253 | /** 254 | * @see https://dom.spec.whatwg.org/#dom-event-composed 255 | */ 256 | get composed(): boolean { 257 | return $(this).composed 258 | } 259 | 260 | /** 261 | * @see https://dom.spec.whatwg.org/#dom-event-istrusted 262 | */ 263 | //istanbul ignore next 264 | get isTrusted(): boolean { 265 | return false 266 | } 267 | 268 | /** 269 | * @see https://dom.spec.whatwg.org/#dom-event-timestamp 270 | */ 271 | get timeStamp(): number { 272 | return $(this).timeStamp 273 | } 274 | 275 | /** 276 | * @deprecated Don't use this method. The constructor did initialization. 277 | */ 278 | initEvent(type: string, bubbles = false, cancelable = false) { 279 | const data = $(this) 280 | if (data.dispatchFlag) { 281 | InitEventWasCalledWhileDispatching.warn() 282 | return 283 | } 284 | 285 | internalDataMap.set(this, { 286 | ...data, 287 | type: String(type), 288 | bubbles: Boolean(bubbles), 289 | cancelable: Boolean(cancelable), 290 | target: null, 291 | currentTarget: null, 292 | stopPropagationFlag: false, 293 | stopImmediatePropagationFlag: false, 294 | canceledFlag: false, 295 | }) 296 | } 297 | } 298 | 299 | /*eslint-enable class-methods-use-this */ 300 | 301 | export namespace Event { 302 | /** 303 | * The options of the `Event` constructor. 304 | * @see https://dom.spec.whatwg.org/#dictdef-eventinit 305 | */ 306 | export interface EventInit { 307 | bubbles?: boolean 308 | cancelable?: boolean 309 | composed?: boolean 310 | } 311 | } 312 | 313 | export { $ as getEventInternalData } 314 | 315 | //------------------------------------------------------------------------------ 316 | // Helpers 317 | //------------------------------------------------------------------------------ 318 | 319 | const NONE = 0 320 | const CAPTURING_PHASE = 1 321 | const AT_TARGET = 2 322 | const BUBBLING_PHASE = 3 323 | 324 | /** 325 | * Private data. 326 | */ 327 | interface EventInternalData { 328 | /** 329 | * The value of `type` attribute. 330 | */ 331 | readonly type: string 332 | /** 333 | * The value of `bubbles` attribute. 334 | */ 335 | readonly bubbles: boolean 336 | /** 337 | * The value of `cancelable` attribute. 338 | */ 339 | readonly cancelable: boolean 340 | /** 341 | * The value of `composed` attribute. 342 | */ 343 | readonly composed: boolean 344 | /** 345 | * The value of `timeStamp` attribute. 346 | */ 347 | readonly timeStamp: number 348 | 349 | /** 350 | * @see https://dom.spec.whatwg.org/#dom-event-target 351 | */ 352 | target: EventTarget | null 353 | /** 354 | * @see https://dom.spec.whatwg.org/#dom-event-currenttarget 355 | */ 356 | currentTarget: EventTarget | null 357 | /** 358 | * @see https://dom.spec.whatwg.org/#stop-propagation-flag 359 | */ 360 | stopPropagationFlag: boolean 361 | /** 362 | * @see https://dom.spec.whatwg.org/#stop-immediate-propagation-flag 363 | */ 364 | stopImmediatePropagationFlag: boolean 365 | /** 366 | * @see https://dom.spec.whatwg.org/#canceled-flag 367 | */ 368 | canceledFlag: boolean 369 | /** 370 | * @see https://dom.spec.whatwg.org/#in-passive-listener-flag 371 | */ 372 | inPassiveListenerFlag: boolean 373 | /** 374 | * @see https://dom.spec.whatwg.org/#dispatch-flag 375 | */ 376 | dispatchFlag: boolean 377 | } 378 | 379 | /** 380 | * Private data for event wrappers. 381 | */ 382 | const internalDataMap = new WeakMap() 383 | 384 | /** 385 | * Get private data. 386 | * @param event The event object to get private data. 387 | * @param name The variable name to report. 388 | * @returns The private data of the event. 389 | */ 390 | function $(event: unknown, name = "this"): EventInternalData { 391 | const retv = internalDataMap.get(event) 392 | assertType( 393 | retv != null, 394 | "'%s' must be an object that Event constructor created, but got another one: %o", 395 | name, 396 | event, 397 | ) 398 | return retv 399 | } 400 | 401 | /** 402 | * https://dom.spec.whatwg.org/#set-the-canceled-flag 403 | * @param data private data. 404 | */ 405 | function setCancelFlag(data: EventInternalData) { 406 | if (data.inPassiveListenerFlag) { 407 | CanceledInPassiveListener.warn() 408 | return 409 | } 410 | if (!data.cancelable) { 411 | NonCancelableEventWasCanceled.warn() 412 | return 413 | } 414 | 415 | data.canceledFlag = true 416 | } 417 | 418 | // Set enumerable 419 | Object.defineProperty(Event, "NONE", { enumerable: true }) 420 | Object.defineProperty(Event, "CAPTURING_PHASE", { enumerable: true }) 421 | Object.defineProperty(Event, "AT_TARGET", { enumerable: true }) 422 | Object.defineProperty(Event, "BUBBLING_PHASE", { enumerable: true }) 423 | const keys = Object.getOwnPropertyNames(Event.prototype) 424 | for (let i = 0; i < keys.length; ++i) { 425 | if (keys[i] === "constructor") { 426 | continue 427 | } 428 | Object.defineProperty(Event.prototype, keys[i], { enumerable: true }) 429 | } 430 | 431 | // Ensure `event instanceof window.Event` is `true`. 432 | if (typeof Global !== "undefined" && typeof Global.Event !== "undefined") { 433 | Object.setPrototypeOf(Event.prototype, Global.Event.prototype) 434 | } 435 | -------------------------------------------------------------------------------- /src/lib/global.ts: -------------------------------------------------------------------------------- 1 | declare const globalThis: any 2 | declare const window: any 3 | declare const self: any 4 | declare const global: any 5 | 6 | /** 7 | * The global object. 8 | */ 9 | //istanbul ignore next 10 | export const Global: any = 11 | typeof window !== "undefined" 12 | ? window 13 | : typeof self !== "undefined" 14 | ? self 15 | : typeof global !== "undefined" 16 | ? global 17 | : typeof globalThis !== "undefined" 18 | ? globalThis 19 | : undefined 20 | -------------------------------------------------------------------------------- /src/lib/legacy.ts: -------------------------------------------------------------------------------- 1 | import { Event } from "./event" 2 | import { 3 | getEventAttributeValue, 4 | setEventAttributeValue, 5 | } from "./event-attribute-handler" 6 | import { EventTarget } from "./event-target" 7 | 8 | /** 9 | * Define an `EventTarget` class that has event attibutes. 10 | * @param types The types to define event attributes. 11 | * @deprecated Use `getEventAttributeValue`/`setEventAttributeValue` pair on your derived class instead because of static analysis friendly. 12 | */ 13 | export function defineCustomEventTarget< 14 | TEventMap extends Record, 15 | TMode extends "standard" | "strict" = "standard" 16 | >( 17 | ...types: (string & keyof TEventMap)[] 18 | ): defineCustomEventTarget.CustomEventTargetConstructor { 19 | class CustomEventTarget extends EventTarget {} 20 | for (let i = 0; i < types.length; ++i) { 21 | defineEventAttribute(CustomEventTarget.prototype, types[i]) 22 | } 23 | 24 | return CustomEventTarget as any 25 | } 26 | 27 | export namespace defineCustomEventTarget { 28 | /** 29 | * The interface of CustomEventTarget constructor. 30 | */ 31 | export type CustomEventTargetConstructor< 32 | TEventMap extends Record, 33 | TMode extends "standard" | "strict" 34 | > = { 35 | /** 36 | * Create a new instance. 37 | */ 38 | new (): CustomEventTarget 39 | /** 40 | * prototype object. 41 | */ 42 | prototype: CustomEventTarget 43 | } 44 | 45 | /** 46 | * The interface of CustomEventTarget. 47 | */ 48 | export type CustomEventTarget< 49 | TEventMap extends Record, 50 | TMode extends "standard" | "strict" 51 | > = EventTarget & 52 | defineEventAttribute.EventAttributes 53 | } 54 | 55 | /** 56 | * Define an event attribute. 57 | * @param target The `EventTarget` object to define an event attribute. 58 | * @param type The event type to define. 59 | * @param _eventClass Unused, but to infer `Event` class type. 60 | * @deprecated Use `getEventAttributeValue`/`setEventAttributeValue` pair on your derived class instead because of static analysis friendly. 61 | */ 62 | export function defineEventAttribute< 63 | TEventTarget extends EventTarget, 64 | TEventType extends string, 65 | TEventConstrucor extends typeof Event 66 | >( 67 | target: TEventTarget, 68 | type: TEventType, 69 | _eventClass?: TEventConstrucor, 70 | ): asserts target is TEventTarget & 71 | defineEventAttribute.EventAttributes< 72 | TEventTarget, 73 | Record> 74 | > { 75 | Object.defineProperty(target, `on${type}`, { 76 | get() { 77 | return getEventAttributeValue(this, type) 78 | }, 79 | set(value) { 80 | setEventAttributeValue(this, type, value) 81 | }, 82 | configurable: true, 83 | enumerable: true, 84 | }) 85 | } 86 | 87 | export namespace defineEventAttribute { 88 | /** 89 | * Definition of event attributes. 90 | */ 91 | export type EventAttributes< 92 | TEventTarget extends EventTarget, 93 | TEventMap extends Record 94 | > = { 95 | [P in string & 96 | keyof TEventMap as `on${P}`]: EventTarget.CallbackFunction< 97 | TEventTarget, 98 | TEventMap[P] 99 | > | null 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/lib/listener-list-map.ts: -------------------------------------------------------------------------------- 1 | import { ListenerList } from "./listener-list" 2 | 3 | /** 4 | * The map from event types to each listener list. 5 | */ 6 | export interface ListenerListMap { 7 | [type: string]: ListenerList | undefined 8 | } 9 | 10 | /** 11 | * Create a new `ListenerListMap` object. 12 | */ 13 | export function createListenerListMap(): ListenerListMap { 14 | return Object.create(null) 15 | } 16 | 17 | /** 18 | * Get the listener list of the given type. 19 | * If the listener list has not been initialized, initialize and return it. 20 | * @param listenerMap The listener list map. 21 | * @param type The event type to get. 22 | */ 23 | export function ensureListenerList( 24 | listenerMap: Record, 25 | type: string, 26 | ): ListenerList { 27 | return (listenerMap[type] ??= { 28 | attrCallback: undefined, 29 | attrListener: undefined, 30 | cow: false, 31 | listeners: [], 32 | }) 33 | } 34 | -------------------------------------------------------------------------------- /src/lib/listener-list.ts: -------------------------------------------------------------------------------- 1 | import { createListener, isCapture, Listener, setRemoved } from "./listener" 2 | 3 | /** 4 | * Information of an listener list. 5 | */ 6 | export interface ListenerList { 7 | /** 8 | * The callback function of the event attribute handler. 9 | */ 10 | attrCallback: Listener.CallbackFunction | undefined 11 | /** 12 | * The listener of the event attribute handler. 13 | */ 14 | attrListener: Listener | undefined 15 | /** 16 | * `true` if the `dispatchEvent` method is traversing the current `listeners` array. 17 | */ 18 | cow: boolean 19 | /** 20 | * The listeners. 21 | * This is writable for copy-on-write. 22 | */ 23 | listeners: Listener[] 24 | } 25 | 26 | /** 27 | * Find the index of given listener. 28 | * This returns `-1` if not found. 29 | * @param list The listener list. 30 | * @param callback The callback function to find. 31 | * @param capture The capture flag to find. 32 | */ 33 | export function findIndexOfListener( 34 | { listeners }: ListenerList, 35 | callback: Listener.Callback, 36 | capture: boolean, 37 | ): number { 38 | for (let i = 0; i < listeners.length; ++i) { 39 | if ( 40 | listeners[i].callback === callback && 41 | isCapture(listeners[i]) === capture 42 | ) { 43 | return i 44 | } 45 | } 46 | return -1 47 | } 48 | 49 | /** 50 | * Add the given listener. 51 | * Does copy-on-write if needed. 52 | * @param list The listener list. 53 | * @param callback The callback function. 54 | * @param capture The capture flag. 55 | * @param passive The passive flag. 56 | * @param once The once flag. 57 | * @param signal The abort signal. 58 | */ 59 | export function addListener( 60 | list: ListenerList, 61 | callback: Listener.Callback, 62 | capture: boolean, 63 | passive: boolean, 64 | once: boolean, 65 | signal: Listener.AbortSignal | undefined, 66 | ): Listener { 67 | let signalListener: (() => void) | undefined 68 | if (signal) { 69 | signalListener = removeListener.bind(null, list, callback, capture) 70 | signal.addEventListener("abort", signalListener) 71 | } 72 | 73 | const listener = createListener( 74 | callback, 75 | capture, 76 | passive, 77 | once, 78 | signal, 79 | signalListener, 80 | ) 81 | 82 | if (list.cow) { 83 | list.cow = false 84 | list.listeners = [...list.listeners, listener] 85 | } else { 86 | list.listeners.push(listener) 87 | } 88 | 89 | return listener 90 | } 91 | 92 | /** 93 | * Remove a listener. 94 | * @param list The listener list. 95 | * @param callback The callback function to find. 96 | * @param capture The capture flag to find. 97 | * @returns `true` if it mutated the list directly. 98 | */ 99 | export function removeListener( 100 | list: ListenerList, 101 | callback: Listener.Callback, 102 | capture: boolean, 103 | ): boolean { 104 | const index = findIndexOfListener(list, callback, capture) 105 | if (index !== -1) { 106 | return removeListenerAt(list, index) 107 | } 108 | return false 109 | } 110 | 111 | /** 112 | * Remove a listener. 113 | * @param list The listener list. 114 | * @param index The index of the target listener. 115 | * @param disableCow Disable copy-on-write if true. 116 | * @returns `true` if it mutated the `listeners` array directly. 117 | */ 118 | export function removeListenerAt( 119 | list: ListenerList, 120 | index: number, 121 | disableCow = false, 122 | ): boolean { 123 | const listener = list.listeners[index] 124 | 125 | // Set the removed flag. 126 | setRemoved(listener) 127 | 128 | // Dispose the abort signal listener if exists. 129 | if (listener.signal) { 130 | listener.signal.removeEventListener("abort", listener.signalListener!) 131 | } 132 | 133 | // Remove it from the array. 134 | if (list.cow && !disableCow) { 135 | list.cow = false 136 | list.listeners = list.listeners.filter((_, i) => i !== index) 137 | return false 138 | } 139 | list.listeners.splice(index, 1) 140 | return true 141 | } 142 | -------------------------------------------------------------------------------- /src/lib/listener.ts: -------------------------------------------------------------------------------- 1 | import { reportError } from "./error-handler" 2 | import { Event } from "./event" // Used as only type, so no circular. 3 | import { EventTarget } from "./event-target" // Used as only type, so no circular. 4 | 5 | /** 6 | * The event listener concept. 7 | * @see https://dom.spec.whatwg.org/#concept-event-listener 8 | */ 9 | export interface Listener { 10 | /** 11 | * The callback function. 12 | */ 13 | readonly callback: Listener.Callback 14 | /** 15 | * The flags of this listener. 16 | * This is writable to add the removed flag. 17 | */ 18 | flags: ListenerFlags 19 | /** 20 | * The `AbortSignal` to remove this listener. 21 | */ 22 | readonly signal: Listener.AbortSignal | undefined 23 | /** 24 | * The `abort` event listener for the `signal`. 25 | * To remove it from the `signal`. 26 | */ 27 | readonly signalListener: (() => void) | undefined 28 | } 29 | 30 | export namespace Listener { 31 | export type Callback< 32 | TEventTarget extends EventTarget, 33 | TEvent extends Event 34 | > = CallbackFunction | CallbackObject 35 | 36 | export interface CallbackFunction< 37 | TEventTarget extends EventTarget, 38 | TEvent extends Event 39 | > { 40 | (this: TEventTarget, event: TEvent): void 41 | } 42 | 43 | export interface CallbackObject { 44 | handleEvent(event: TEvent): void 45 | } 46 | 47 | export interface AbortSignal { 48 | addEventListener(type: string, callback: Callback): void 49 | removeEventListener(type: string, callback: Callback): void 50 | } 51 | } 52 | 53 | /** 54 | * Create a new listener. 55 | * @param callback The callback function. 56 | * @param capture The capture flag. 57 | * @param passive The passive flag. 58 | * @param once The once flag. 59 | * @param signal The abort signal. 60 | * @param signalListener The abort event listener for the abort signal. 61 | */ 62 | export function createListener( 63 | callback: Listener.Callback, 64 | capture: boolean, 65 | passive: boolean, 66 | once: boolean, 67 | signal: Listener.AbortSignal | undefined, 68 | signalListener: (() => void) | undefined, 69 | ): Listener { 70 | return { 71 | callback, 72 | flags: 73 | (capture ? ListenerFlags.Capture : 0) | 74 | (passive ? ListenerFlags.Passive : 0) | 75 | (once ? ListenerFlags.Once : 0), 76 | signal, 77 | signalListener, 78 | } 79 | } 80 | 81 | /** 82 | * Set the `removed` flag to the given listener. 83 | * @param listener The listener to check. 84 | */ 85 | export function setRemoved(listener: Listener): void { 86 | listener.flags |= ListenerFlags.Removed 87 | } 88 | 89 | /** 90 | * Check if the given listener has the `capture` flag or not. 91 | * @param listener The listener to check. 92 | */ 93 | export function isCapture(listener: Listener): boolean { 94 | return (listener.flags & ListenerFlags.Capture) === ListenerFlags.Capture 95 | } 96 | 97 | /** 98 | * Check if the given listener has the `passive` flag or not. 99 | * @param listener The listener to check. 100 | */ 101 | export function isPassive(listener: Listener): boolean { 102 | return (listener.flags & ListenerFlags.Passive) === ListenerFlags.Passive 103 | } 104 | 105 | /** 106 | * Check if the given listener has the `once` flag or not. 107 | * @param listener The listener to check. 108 | */ 109 | export function isOnce(listener: Listener): boolean { 110 | return (listener.flags & ListenerFlags.Once) === ListenerFlags.Once 111 | } 112 | 113 | /** 114 | * Check if the given listener has the `removed` flag or not. 115 | * @param listener The listener to check. 116 | */ 117 | export function isRemoved(listener: Listener): boolean { 118 | return (listener.flags & ListenerFlags.Removed) === ListenerFlags.Removed 119 | } 120 | 121 | /** 122 | * Call an event listener. 123 | * @param listener The listener to call. 124 | * @param target The event target object for `thisArg`. 125 | * @param event The event object for the first argument. 126 | * @param attribute `true` if this callback is an event attribute handler. 127 | */ 128 | export function invokeCallback( 129 | { callback }: Listener, 130 | target: EventTarget, 131 | event: Event, 132 | ): void { 133 | try { 134 | if (typeof callback === "function") { 135 | callback.call(target, event) 136 | } else if (typeof callback.handleEvent === "function") { 137 | callback.handleEvent(event) 138 | } 139 | } catch (thrownError) { 140 | reportError(thrownError) 141 | } 142 | } 143 | 144 | //------------------------------------------------------------------------------ 145 | // Helpers 146 | //------------------------------------------------------------------------------ 147 | 148 | /** 149 | * The flags of listeners. 150 | */ 151 | const enum ListenerFlags { 152 | Capture = 0x01, 153 | Passive = 0x02, 154 | Once = 0x04, 155 | Removed = 0x08, 156 | } 157 | -------------------------------------------------------------------------------- /src/lib/misc.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Assert a condition. 3 | * @param condition The condition that it should satisfy. 4 | * @param message The error message. 5 | * @param args The arguments for replacing placeholders in the message. 6 | */ 7 | export function assertType( 8 | condition: boolean, 9 | message: string, 10 | ...args: any[] 11 | ): asserts condition { 12 | if (!condition) { 13 | throw new TypeError(format(message, args)) 14 | } 15 | } 16 | 17 | /** 18 | * Convert a text and arguments to one string. 19 | * @param message The formating text 20 | * @param args The arguments. 21 | */ 22 | export function format(message: string, args: any[]): string { 23 | let i = 0 24 | return message.replace(/%[os]/gu, () => anyToString(args[i++])) 25 | } 26 | 27 | /** 28 | * Convert a value to a string representation. 29 | * @param x The value to get the string representation. 30 | */ 31 | export function anyToString(x: any): string { 32 | if (typeof x !== "object" || x === null) { 33 | return String(x) 34 | } 35 | return Object.prototype.toString.call(x) 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/warning-handler.ts: -------------------------------------------------------------------------------- 1 | import { assertType } from "./misc" 2 | 3 | declare const console: any 4 | 5 | let currentWarnHandler: setWarningHandler.WarningHandler | undefined 6 | 7 | /** 8 | * Set the warning handler. 9 | * @param value The warning handler to set. 10 | */ 11 | export function setWarningHandler( 12 | value: setWarningHandler.WarningHandler | undefined, 13 | ): void { 14 | assertType( 15 | typeof value === "function" || value === undefined, 16 | "The warning handler must be a function or undefined, but got %o.", 17 | value, 18 | ) 19 | currentWarnHandler = value 20 | } 21 | export namespace setWarningHandler { 22 | /** 23 | * The warning information. 24 | */ 25 | export interface Warning { 26 | /** 27 | * The code of this warning. 28 | */ 29 | code: string 30 | /** 31 | * The message in English. 32 | */ 33 | message: string 34 | /** 35 | * The arguments for replacing placeholders in the text. 36 | */ 37 | args: any[] 38 | } 39 | 40 | /** 41 | * The warning handler. 42 | * @param warning The warning. 43 | */ 44 | export type WarningHandler = (warning: Warning) => void 45 | } 46 | 47 | /** 48 | * The warning information. 49 | */ 50 | export class Warning { 51 | readonly code: string 52 | readonly message: string 53 | 54 | constructor(code: string, message: string) { 55 | this.code = code 56 | this.message = message 57 | } 58 | 59 | /** 60 | * Report this warning. 61 | * @param args The arguments of the warning. 62 | */ 63 | warn(...args: TArgs): void { 64 | try { 65 | // Call the user-defined warning handler if exists. 66 | if (currentWarnHandler) { 67 | currentWarnHandler({ ...this, args }) 68 | return 69 | } 70 | 71 | // Otherwise, print the warning. 72 | const stack = (new Error().stack ?? "").replace( 73 | /^(?:.+?\n){2}/gu, 74 | "\n", 75 | ) 76 | console.warn(this.message, ...args, stack) 77 | } catch { 78 | // Ignore. 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/lib/warnings.ts: -------------------------------------------------------------------------------- 1 | import { EventTarget } from "./event-target" // Used as only type, so no circular. 2 | import { Warning } from "./warning-handler" 3 | 4 | export const InitEventWasCalledWhileDispatching = new Warning<[]>( 5 | "W01", 6 | "Unable to initialize event under dispatching.", 7 | ) 8 | 9 | export const FalsyWasAssignedToCancelBubble = new Warning<[]>( 10 | "W02", 11 | "Assigning any falsy value to 'cancelBubble' property has no effect.", 12 | ) 13 | 14 | export const TruthyWasAssignedToReturnValue = new Warning<[]>( 15 | "W03", 16 | "Assigning any truthy value to 'returnValue' property has no effect.", 17 | ) 18 | 19 | export const NonCancelableEventWasCanceled = new Warning<[]>( 20 | "W04", 21 | "Unable to preventDefault on non-cancelable events.", 22 | ) 23 | 24 | export const CanceledInPassiveListener = new Warning<[]>( 25 | "W05", 26 | "Unable to preventDefault inside passive event listener invocation.", 27 | ) 28 | 29 | export const EventListenerWasDuplicated = new Warning< 30 | [type: "bubble" | "capture", callback: EventTarget.EventListener] 31 | >( 32 | "W06", 33 | "An event listener wasn't added because it has been added already: %o, %o", 34 | ) 35 | 36 | export const OptionWasIgnored = new Warning< 37 | [name: "passive" | "once" | "signal"] 38 | >( 39 | "W07", 40 | "The %o option value was abandoned because the event listener wasn't added as duplicated.", 41 | ) 42 | 43 | export const InvalidEventListener = new Warning< 44 | [callback: EventTarget.EventListener | {} | null | undefined] 45 | >( 46 | "W08", 47 | "The 'callback' argument must be a function or an object that has 'handleEvent' method: %o", 48 | ) 49 | 50 | export const InvalidAttributeHandler = new Warning< 51 | [callback: EventTarget.EventListener | {}] 52 | >("W09", "Event attribute handler must be a function: %o") 53 | -------------------------------------------------------------------------------- /test/default-error-handler.ts: -------------------------------------------------------------------------------- 1 | import { spy } from "@mysticatea/spy" 2 | import assert from "assert" 3 | import { Event, EventTarget, setWarningHandler } from "../src/index" 4 | 5 | describe("The default error handler", () => { 6 | const onBrowser = 7 | typeof window !== "undefined" && 8 | typeof window.dispatchEvent === "function" 9 | const onNode = 10 | !onBrowser && 11 | typeof process !== "undefined" && 12 | typeof process.emit === "function" 13 | 14 | beforeEach(() => { 15 | setWarningHandler(() => {}) 16 | }) 17 | afterEach(() => { 18 | setWarningHandler(undefined) 19 | }) 20 | 21 | // 22 | ;(onBrowser ? describe : xdescribe)("on a browser", () => { 23 | it("should dispatch an ErrorEvent if a listener threw an error", () => { 24 | const originalConsoleError = console.error 25 | const f = spy((_message, _source, _lineno, _colno, _error) => {}) 26 | const consoleError = spy((...args: any[]) => {}) 27 | const target = new EventTarget() 28 | const error = new Error("test error") 29 | target.addEventListener("foo", () => { 30 | throw error 31 | }) 32 | 33 | window.onerror = f 34 | console.error = consoleError 35 | try { 36 | target.dispatchEvent(new Event("foo")) 37 | } finally { 38 | window.onerror = null 39 | console.error = originalConsoleError 40 | } 41 | 42 | assert.strictEqual(f.calls.length, 1, "f should be called.") 43 | assert.strictEqual(f.calls[0].arguments[0], error.message) 44 | assert.strictEqual(f.calls[0].arguments[4], error) 45 | assert.strictEqual( 46 | consoleError.calls.length, 47 | 1, 48 | "console.error should be called.", 49 | ) 50 | assert.strictEqual(consoleError.calls[0].arguments[0], error) 51 | }) 52 | }) 53 | 54 | // 55 | ;(onNode ? describe : xdescribe)("on Node.js", () => { 56 | let mochaListener: any 57 | 58 | // Remove mocha's `uncaughtException` handler while this test case 59 | // because it expects `uncaughtException` to be thrown. 60 | beforeEach(() => { 61 | mochaListener = process.listeners("uncaughtException").pop() 62 | process.removeListener("uncaughtException", mochaListener) 63 | }) 64 | afterEach(() => { 65 | process.addListener("uncaughtException", mochaListener) 66 | }) 67 | 68 | it("should emit an uncaughtException event if a listener threw an error", () => { 69 | const f = spy(_event => {}) 70 | const target = new EventTarget() 71 | const error = new Error("test error") 72 | target.addEventListener("foo", () => { 73 | throw error 74 | }) 75 | 76 | process.on("uncaughtException", f) 77 | target.dispatchEvent(new Event("foo")) 78 | process.removeListener("uncaughtException", f) 79 | 80 | assert.strictEqual(f.calls.length, 1, "f should be called.") 81 | assert.strictEqual(f.calls[0].arguments[0], error) 82 | }) 83 | }) 84 | }) 85 | -------------------------------------------------------------------------------- /test/default-warning-handler.ts: -------------------------------------------------------------------------------- 1 | import { spy } from "@mysticatea/spy" 2 | import assert from "assert" 3 | import { EventTarget } from "../src/index" 4 | import { InvalidEventListener } from "../src/lib/warnings" 5 | 6 | describe("The default warning handler", () => { 7 | it("should print the warning by 'console.warn'.", () => { 8 | /*eslint-disable no-console */ 9 | const originalWarn = console.warn 10 | const f = spy((...args: any[]) => {}) 11 | const target = new EventTarget() 12 | 13 | console.warn = f 14 | target.addEventListener("foo") 15 | console.warn = originalWarn 16 | 17 | assert.strictEqual(f.calls.length, 1, "f should be called.") 18 | assert.strictEqual( 19 | f.calls[0].arguments[0], 20 | InvalidEventListener.message, 21 | ) 22 | assert.strictEqual(f.calls[0].arguments[1], undefined) 23 | /*eslint-enable no-console */ 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /test/define-custom-event-target.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import { defineCustomEventTarget, Event, EventTarget } from "../src/index" 3 | import { countEventListeners } from "./lib/count-event-listeners" 4 | import { setupErrorCheck } from "./lib/setup-error-check" 5 | 6 | describe("'defineCustomEventTarget' function", () => { 7 | setupErrorCheck() 8 | 9 | describe("when '{foo:Event; bar:Event}' type argument is present, the returned valuu is", () => { 10 | const MyEventTarget = defineCustomEventTarget<{ 11 | foo: Event<"foo"> 12 | bar: Event<"bar"> 13 | }>("foo", "bar") 14 | type MyEventTarget = InstanceType 15 | 16 | it("should be a function.", () => { 17 | assert.strictEqual(typeof MyEventTarget, "function") 18 | }) 19 | 20 | it("should throw a TypeError on function calls.", () => { 21 | assert.throws(() => { 22 | // @ts-expect-error 23 | MyEventTarget() // eslint-disable-line new-cap 24 | }, TypeError) 25 | }) 26 | 27 | it("should return an instance on constructor calls.", () => { 28 | const target = new MyEventTarget() 29 | assert( 30 | target instanceof MyEventTarget, 31 | "should be an instance of MyEventTarget", 32 | ) 33 | assert( 34 | target instanceof EventTarget, 35 | "should be an instance of EventTarget", 36 | ) 37 | }) 38 | 39 | describe("the instance of MyEventTarget", () => { 40 | let target: MyEventTarget 41 | beforeEach(() => { 42 | target = new MyEventTarget() 43 | }) 44 | 45 | describe("'onfoo' property", () => { 46 | it("should be null at first", () => { 47 | assert.strictEqual(target.onfoo, null) 48 | }) 49 | 50 | it("should be able to set a function", () => { 51 | const f = () => {} 52 | target.onfoo = f 53 | assert.strictEqual(target.onfoo, f) 54 | }) 55 | 56 | it("should add an listener on setting a function", () => { 57 | const f = () => {} 58 | target.onfoo = f 59 | assert.strictEqual(countEventListeners(target, "foo"), 1) 60 | }) 61 | 62 | it("should remove the set listener on setting null", () => { 63 | const f = () => {} 64 | target.onfoo = f 65 | assert.strictEqual(countEventListeners(target, "foo"), 1) 66 | target.onfoo = null 67 | assert.strictEqual(countEventListeners(target, "foo"), 0) 68 | }) 69 | }) 70 | 71 | describe("'onbar' property", () => { 72 | it("should be null at first", () => { 73 | assert.strictEqual(target.onbar, null) 74 | }) 75 | 76 | it("should be able to set a function", () => { 77 | const f = () => {} 78 | target.onbar = f 79 | assert.strictEqual(target.onbar, f) 80 | }) 81 | 82 | it("should add an listener on setting a function", () => { 83 | const f = () => {} 84 | target.onbar = f 85 | assert.strictEqual(countEventListeners(target, "bar"), 1) 86 | }) 87 | 88 | it("should remove the set listener on setting null", () => { 89 | const f = () => {} 90 | target.onbar = f 91 | assert.strictEqual(countEventListeners(target, "bar"), 1) 92 | target.onbar = null 93 | assert.strictEqual(countEventListeners(target, "bar"), 0) 94 | }) 95 | }) 96 | 97 | describe("for-in", () => { 98 | it("should enumerate 5 property names", () => { 99 | const actualKeys = [] 100 | const expectedKeys = [ 101 | "addEventListener", 102 | "removeEventListener", 103 | "dispatchEvent", 104 | "onfoo", 105 | "onbar", 106 | ] 107 | 108 | // eslint-disable-next-line @mysticatea/prefer-for-of 109 | for (const key in target) { 110 | actualKeys.push(key) 111 | } 112 | 113 | assert.deepStrictEqual( 114 | actualKeys.sort(undefined), 115 | expectedKeys.sort(undefined), 116 | ) 117 | }) 118 | }) 119 | }) 120 | }) 121 | }) 122 | -------------------------------------------------------------------------------- /test/event-attribute.ts: -------------------------------------------------------------------------------- 1 | import { spy } from "@mysticatea/spy" 2 | import assert from "assert" 3 | import { 4 | Event, 5 | EventTarget, 6 | getEventAttributeValue, 7 | setEventAttributeValue, 8 | } from "../src/index" 9 | import { InvalidAttributeHandler } from "../src/lib/warnings" 10 | import { countEventListeners } from "./lib/count-event-listeners" 11 | import { setupErrorCheck } from "./lib/setup-error-check" 12 | 13 | describe("Event attribute handlers", () => { 14 | const { assertWarning } = setupErrorCheck() 15 | 16 | let target: EventTarget 17 | beforeEach(() => { 18 | target = new EventTarget() 19 | }) 20 | 21 | describe("'getEventAttributeValue' function", () => { 22 | it("should throw a TypeError if non-EventTarget object is present", () => { 23 | assert.throws(() => { 24 | // @ts-expect-error 25 | getEventAttributeValue() 26 | }, TypeError) 27 | assert.throws(() => { 28 | // @ts-expect-error 29 | getEventAttributeValue(null) 30 | }, TypeError) 31 | assert.throws(() => { 32 | // @ts-expect-error 33 | getEventAttributeValue({}) 34 | }, TypeError) 35 | }) 36 | 37 | it("should return null if any handlers are not set.", () => { 38 | assert.strictEqual(getEventAttributeValue(target, "foo"), null) 39 | }) 40 | 41 | it("should return null if any handlers are not set, even if listeners are added by 'addEventListener'.", () => { 42 | target.addEventListener("foo", () => {}) 43 | assert.strictEqual(getEventAttributeValue(target, "foo"), null) 44 | }) 45 | 46 | it("should return null if listeners are set to a different event by 'setEventAttributeValue'.", () => { 47 | const f = () => {} 48 | setEventAttributeValue(target, "bar", f) 49 | assert.strictEqual(getEventAttributeValue(target, "foo"), null) 50 | }) 51 | 52 | it("should return the set function if listeners are set by 'setEventAttributeValue'.", () => { 53 | const f = () => {} 54 | setEventAttributeValue(target, "foo", f) 55 | assert.strictEqual(getEventAttributeValue(target, "foo"), f) 56 | }) 57 | 58 | it("should return the set object if listeners are set by 'setEventAttributeValue'.", () => { 59 | const f = {} 60 | // @ts-expect-error 61 | setEventAttributeValue(target, "foo", f) 62 | assert.strictEqual(getEventAttributeValue(target, "foo"), f) 63 | assertWarning(InvalidAttributeHandler, f) 64 | }) 65 | 66 | it("should return the last set function if listeners are set by 'setEventAttributeValue' multiple times.", () => { 67 | const f = () => {} 68 | setEventAttributeValue(target, "foo", () => {}) 69 | setEventAttributeValue(target, "foo", null) 70 | setEventAttributeValue(target, "foo", () => {}) 71 | setEventAttributeValue(target, "foo", f) 72 | assert.strictEqual(getEventAttributeValue(target, "foo"), f) 73 | }) 74 | 75 | it("should handle the string representation of the type argument", () => { 76 | const f = () => {} 77 | setEventAttributeValue(target, "1000", f) 78 | // @ts-expect-error 79 | assert.strictEqual(getEventAttributeValue(target, 1e3), f) 80 | }) 81 | }) 82 | 83 | describe("'setEventAttributeValue' function", () => { 84 | it("should throw a TypeError if non-EventTarget object is present", () => { 85 | assert.throws(() => { 86 | // @ts-expect-error 87 | setEventAttributeValue() 88 | }, TypeError) 89 | assert.throws(() => { 90 | // @ts-expect-error 91 | setEventAttributeValue(null) 92 | }, TypeError) 93 | assert.throws(() => { 94 | // @ts-expect-error 95 | setEventAttributeValue({}) 96 | }, TypeError) 97 | }) 98 | 99 | it("should add an event listener if a function is given.", () => { 100 | setEventAttributeValue(target, "foo", () => {}) 101 | assert.strictEqual(countEventListeners(target), 1) 102 | assert.strictEqual(countEventListeners(target, "foo"), 1) 103 | }) 104 | 105 | it("should add an event listener if an object is given.", () => { 106 | const f = {} 107 | // @ts-expect-error 108 | setEventAttributeValue(target, "foo", f) 109 | assert.strictEqual(countEventListeners(target), 1) 110 | assert.strictEqual(countEventListeners(target, "foo"), 1) 111 | assertWarning(InvalidAttributeHandler, f) 112 | }) 113 | 114 | it("should remove an event listener if null is given.", () => { 115 | setEventAttributeValue(target, "foo", () => {}) 116 | assert.strictEqual(countEventListeners(target, "foo"), 1) 117 | setEventAttributeValue(target, "foo", null) 118 | assert.strictEqual(countEventListeners(target, "foo"), 0) 119 | }) 120 | 121 | it("should remove an event listener if primitive is given.", () => { 122 | setEventAttributeValue(target, "foo", () => {}) 123 | assert.strictEqual(countEventListeners(target, "foo"), 1) 124 | // @ts-expect-error 125 | setEventAttributeValue(target, "foo", 3) 126 | assert.strictEqual(countEventListeners(target, "foo"), 0) 127 | 128 | assertWarning(InvalidAttributeHandler, 3) 129 | }) 130 | 131 | it("should do nothing if primitive is given and the target doesn't have listeners.", () => { 132 | setEventAttributeValue(target, "foo", null) 133 | assert.strictEqual(countEventListeners(target, "foo"), 0) 134 | }) 135 | 136 | it("should handle the string representation of the type argument", () => { 137 | const f = () => {} 138 | // @ts-expect-error 139 | setEventAttributeValue(target, 1e3, f) 140 | 141 | assert.strictEqual(countEventListeners(target), 1) 142 | assert.strictEqual(countEventListeners(target, "1000"), 1) 143 | }) 144 | 145 | it("should keep the added order: attr, normal, capture", () => { 146 | const list: string[] = [] 147 | const f1 = () => { 148 | list.push("f1") 149 | } 150 | const f2 = () => { 151 | list.push("f2") 152 | } 153 | const f3 = () => { 154 | list.push("f3") 155 | } 156 | 157 | setEventAttributeValue(target, "foo", f1) 158 | target.addEventListener("foo", f2) 159 | target.addEventListener("foo", f3, { capture: true }) 160 | target.dispatchEvent(new Event("foo")) 161 | 162 | assert.deepStrictEqual(list, ["f1", "f2", "f3"]) 163 | }) 164 | 165 | it("should keep the added order: normal, capture, attr", () => { 166 | const list: string[] = [] 167 | const f1 = () => { 168 | list.push("f1") 169 | } 170 | const f2 = () => { 171 | list.push("f2") 172 | } 173 | const f3 = () => { 174 | list.push("f3") 175 | } 176 | 177 | target.addEventListener("foo", f1) 178 | target.addEventListener("foo", f2, { capture: true }) 179 | setEventAttributeValue(target, "foo", f3) 180 | target.dispatchEvent(new Event("foo")) 181 | 182 | assert.deepStrictEqual(list, ["f1", "f2", "f3"]) 183 | }) 184 | 185 | it("should keep the added order: capture, attr, normal", () => { 186 | const list: string[] = [] 187 | const f1 = () => { 188 | list.push("f1") 189 | } 190 | const f2 = () => { 191 | list.push("f2") 192 | } 193 | const f3 = () => { 194 | list.push("f3") 195 | } 196 | 197 | target.addEventListener("foo", f1, { capture: true }) 198 | setEventAttributeValue(target, "foo", f2) 199 | target.addEventListener("foo", f3) 200 | target.dispatchEvent(new Event("foo")) 201 | 202 | assert.deepStrictEqual(list, ["f1", "f2", "f3"]) 203 | }) 204 | 205 | it("should not be called by 'dispatchEvent' if the listener is object listener", () => { 206 | const f = { handleEvent: spy() } 207 | // @ts-expect-error 208 | setEventAttributeValue(target, "foo", f) 209 | target.dispatchEvent(new Event("foo")) 210 | 211 | assert.strictEqual( 212 | f.handleEvent.calls.length, 213 | 0, 214 | "handleEvent should not be called", 215 | ) 216 | assertWarning(InvalidAttributeHandler, f) 217 | }) 218 | }) 219 | }) 220 | -------------------------------------------------------------------------------- /test/event.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import { Event, EventTarget } from "../src/index" 3 | import { Global } from "../src/lib/global" 4 | import { 5 | FalsyWasAssignedToCancelBubble, 6 | InitEventWasCalledWhileDispatching, 7 | NonCancelableEventWasCanceled, 8 | TruthyWasAssignedToReturnValue, 9 | } from "../src/lib/warnings" 10 | import { setupErrorCheck } from "./lib/setup-error-check" 11 | 12 | const NativeEvent: typeof Event = Global.Event 13 | 14 | describe("'Event' class", () => { 15 | const { assertWarning } = setupErrorCheck() 16 | 17 | describe("constructor", () => { 18 | it("should return an Event object", () => { 19 | assert(new Event("") instanceof Event) 20 | }) 21 | 22 | it("should throw a TypeError if called as a function", () => { 23 | assert.throws(() => { 24 | // @ts-expect-error 25 | Event("") // eslint-disable-line new-cap 26 | }) 27 | }) 28 | 29 | const nativeDescribe = NativeEvent ? describe : xdescribe 30 | nativeDescribe("if native Event class is present", () => { 31 | it("`event instanceof window.Event` should be true", () => { 32 | const event = new Event("") 33 | assert(event instanceof NativeEvent) 34 | }) 35 | }) 36 | }) 37 | 38 | describe("'type' property", () => { 39 | it("should be the value of the constructor's first argument", () => { 40 | const event = new Event("foo") 41 | assert.strictEqual(event.type, "foo") 42 | }) 43 | 44 | it("should be the string representation of the constructor's first argument", () => { 45 | // @ts-expect-error 46 | assert.strictEqual(new Event().type, "undefined") 47 | // @ts-expect-error 48 | assert.strictEqual(new Event(null).type, "null") 49 | // @ts-expect-error 50 | assert.strictEqual(new Event(1e3).type, "1000") 51 | }) 52 | 53 | it("should be readonly", () => { 54 | const event = new Event("foo") 55 | assert.throws(() => { 56 | // @ts-expect-error 57 | event.type = "bar" 58 | }) 59 | }) 60 | }) 61 | 62 | describe("'target' property", () => { 63 | it("should be null", () => { 64 | const event = new Event("foo") 65 | assert.strictEqual(event.target, null) 66 | }) 67 | 68 | it("should be readonly", () => { 69 | const event = new Event("foo") 70 | assert.throws(() => { 71 | // @ts-expect-error 72 | event.target = null 73 | }) 74 | }) 75 | 76 | it("should be the event target under dispatching", () => { 77 | const target = new EventTarget() 78 | const event = new Event("foo") 79 | let ok = false 80 | 81 | target.addEventListener("foo", () => { 82 | assert.strictEqual(event.target, target) 83 | ok = true 84 | }) 85 | target.dispatchEvent(event) 86 | 87 | assert.strictEqual(event.target, null) 88 | assert(ok) 89 | }) 90 | }) 91 | 92 | describe("'srcElement' property", () => { 93 | it("should be null", () => { 94 | const event = new Event("foo") 95 | assert.strictEqual(event.srcElement, null) 96 | }) 97 | 98 | it("should be readonly", () => { 99 | const event = new Event("foo") 100 | assert.throws(() => { 101 | // @ts-expect-error 102 | event.srcElement = null 103 | }) 104 | }) 105 | 106 | it("should be the event target under dispatching", () => { 107 | const target = new EventTarget() 108 | const event = new Event("foo") 109 | let ok = false 110 | 111 | target.addEventListener("foo", () => { 112 | assert.strictEqual(event.srcElement, target) 113 | ok = true 114 | }) 115 | target.dispatchEvent(event) 116 | 117 | assert.strictEqual(event.srcElement, null) 118 | assert(ok) 119 | }) 120 | }) 121 | 122 | describe("'currentTarget' property", () => { 123 | it("should be null", () => { 124 | const event = new Event("foo") 125 | assert.strictEqual(event.currentTarget, null) 126 | }) 127 | 128 | it("should be readonly", () => { 129 | const event = new Event("foo") 130 | assert.throws(() => { 131 | // @ts-expect-error 132 | event.currentTarget = null 133 | }) 134 | }) 135 | 136 | it("should be the event target under dispatching", () => { 137 | const target = new EventTarget() 138 | const event = new Event("foo") 139 | let ok = false 140 | 141 | target.addEventListener("foo", () => { 142 | assert.strictEqual(event.currentTarget, target) 143 | ok = true 144 | }) 145 | target.dispatchEvent(event) 146 | 147 | assert.strictEqual(event.currentTarget, null) 148 | assert(ok) 149 | }) 150 | }) 151 | 152 | describe("'composedPath' method", () => { 153 | it("should return an empty array", () => { 154 | const event = new Event("foo") 155 | assert.deepStrictEqual(event.composedPath(), []) 156 | }) 157 | 158 | it("should return the event target under dispatching", () => { 159 | const target = new EventTarget() 160 | const event = new Event("foo") 161 | let ok = false 162 | 163 | target.addEventListener("foo", () => { 164 | assert.deepStrictEqual(event.composedPath(), [target]) 165 | ok = true 166 | }) 167 | target.dispatchEvent(event) 168 | 169 | assert.deepStrictEqual(event.composedPath(), []) 170 | assert(ok) 171 | }) 172 | }) 173 | 174 | describe("'NONE' property", () => { 175 | it("should be 0", () => { 176 | const event = new Event("foo") 177 | assert.strictEqual(event.NONE, 0) 178 | }) 179 | 180 | it("should be readonly", () => { 181 | const event = new Event("foo") 182 | assert.throws(() => { 183 | // @ts-expect-error 184 | event.NONE = -1 185 | }) 186 | }) 187 | }) 188 | 189 | describe("'NONE' static property", () => { 190 | it("should be 0", () => { 191 | assert.strictEqual(Event.NONE, 0) 192 | }) 193 | 194 | it("should be readonly", () => { 195 | assert.throws(() => { 196 | // @ts-expect-error 197 | Event.NONE = -1 198 | }) 199 | }) 200 | }) 201 | 202 | describe("'CAPTURING_PHASE' property", () => { 203 | it("should be 1", () => { 204 | const event = new Event("foo") 205 | assert.strictEqual(event.CAPTURING_PHASE, 1) 206 | }) 207 | 208 | it("should be readonly", () => { 209 | const event = new Event("foo") 210 | assert.throws(() => { 211 | // @ts-expect-error 212 | event.CAPTURING_PHASE = -1 213 | }) 214 | }) 215 | }) 216 | 217 | describe("'CAPTURING_PHASE' static property", () => { 218 | it("should be 1", () => { 219 | assert.strictEqual(Event.CAPTURING_PHASE, 1) 220 | }) 221 | 222 | it("should be readonly", () => { 223 | assert.throws(() => { 224 | // @ts-expect-error 225 | Event.CAPTURING_PHASE = -1 226 | }) 227 | }) 228 | }) 229 | 230 | describe("'AT_TARGET' property", () => { 231 | it("should be 2", () => { 232 | const event = new Event("foo") 233 | assert.strictEqual(event.AT_TARGET, 2) 234 | }) 235 | 236 | it("should be readonly", () => { 237 | const event = new Event("foo") 238 | assert.throws(() => { 239 | // @ts-expect-error 240 | event.AT_TARGET = -1 241 | }) 242 | }) 243 | }) 244 | 245 | describe("'AT_TARGET' static property", () => { 246 | it("should be 2", () => { 247 | assert.strictEqual(Event.AT_TARGET, 2) 248 | }) 249 | 250 | it("should be readonly", () => { 251 | assert.throws(() => { 252 | // @ts-expect-error 253 | Event.AT_TARGET = -1 254 | }) 255 | }) 256 | }) 257 | 258 | describe("'BUBBLING_PHASE' property", () => { 259 | it("should be 3", () => { 260 | const event = new Event("foo") 261 | assert.strictEqual(event.BUBBLING_PHASE, 3) 262 | }) 263 | 264 | it("should be readonly", () => { 265 | const event = new Event("foo") 266 | assert.throws(() => { 267 | // @ts-expect-error 268 | event.BUBBLING_PHASE = -1 269 | }) 270 | }) 271 | }) 272 | 273 | describe("'BUBBLING_PHASE' static property", () => { 274 | it("should be 3", () => { 275 | assert.strictEqual(Event.BUBBLING_PHASE, 3) 276 | }) 277 | 278 | it("should be readonly", () => { 279 | assert.throws(() => { 280 | // @ts-expect-error 281 | Event.BUBBLING_PHASE = -1 282 | }) 283 | }) 284 | }) 285 | 286 | describe("'eventPhase' property", () => { 287 | it("should be 0", () => { 288 | const event = new Event("foo") 289 | assert.strictEqual(event.eventPhase, 0) 290 | }) 291 | 292 | it("should be readonly", () => { 293 | const event = new Event("foo") 294 | assert.throws(() => { 295 | // @ts-expect-error 296 | event.eventPhase = -1 297 | }) 298 | }) 299 | 300 | it("should be 2 under dispatching", () => { 301 | const target = new EventTarget() 302 | const event = new Event("foo") 303 | let ok = false 304 | 305 | target.addEventListener("foo", () => { 306 | assert.strictEqual(event.eventPhase, 2) 307 | ok = true 308 | }) 309 | target.dispatchEvent(event) 310 | 311 | assert.strictEqual(event.eventPhase, 0) 312 | assert(ok) 313 | }) 314 | }) 315 | 316 | describe("'stopPropagation' method", () => { 317 | it("should return undefined", () => { 318 | const event = new Event("foo") 319 | assert.strictEqual(event.stopPropagation(), undefined) 320 | }) 321 | }) 322 | 323 | describe("'cancelBubble' property", () => { 324 | it("should be false", () => { 325 | const event = new Event("foo") 326 | assert.strictEqual(event.cancelBubble, false) 327 | }) 328 | 329 | it("should be true after 'stopPropagation' method was called", () => { 330 | const event = new Event("foo") 331 | event.stopPropagation() 332 | assert.strictEqual(event.cancelBubble, true) 333 | }) 334 | 335 | it("should be true after 'stopImmediatePropagation' method was called", () => { 336 | const event = new Event("foo") 337 | event.stopImmediatePropagation() 338 | assert.strictEqual(event.cancelBubble, true) 339 | }) 340 | 341 | it("should be writable", () => { 342 | const event = new Event("foo") 343 | event.cancelBubble = true 344 | assert.strictEqual(event.cancelBubble, true) 345 | }) 346 | 347 | it("should NOT be changed by the assignment of false after 'stopPropagation' method was called", () => { 348 | const event = new Event("foo") 349 | event.stopPropagation() 350 | event.cancelBubble = false 351 | assert.strictEqual(event.cancelBubble, true) 352 | assertWarning(FalsyWasAssignedToCancelBubble) 353 | }) 354 | 355 | it("should NOT be changed by the assignment of false after 'stopImmediatePropagation' method was called", () => { 356 | const event = new Event("foo") 357 | event.stopImmediatePropagation() 358 | event.cancelBubble = false 359 | assert.strictEqual(event.cancelBubble, true) 360 | assertWarning(FalsyWasAssignedToCancelBubble) 361 | }) 362 | 363 | it("should NOT be changed by the assignment of false after the assignment of true", () => { 364 | const event = new Event("foo") 365 | event.cancelBubble = true 366 | event.cancelBubble = false 367 | assert.strictEqual(event.cancelBubble, true) 368 | assertWarning(FalsyWasAssignedToCancelBubble) 369 | }) 370 | }) 371 | 372 | describe("'stopImmediatePropagation' method", () => { 373 | it("should return undefined", () => { 374 | const event = new Event("foo") 375 | assert.strictEqual(event.stopImmediatePropagation(), undefined) 376 | }) 377 | }) 378 | 379 | describe("'bubbles' property", () => { 380 | it("should be false if the constructor option was not present", () => { 381 | const event = new Event("foo") 382 | assert.strictEqual(event.bubbles, false) 383 | }) 384 | 385 | it("should be false if the constructor option was false", () => { 386 | const event = new Event("foo", { bubbles: false }) 387 | assert.strictEqual(event.bubbles, false) 388 | }) 389 | 390 | it("should be true if the constructor option was true", () => { 391 | const event = new Event("foo", { bubbles: true }) 392 | assert.strictEqual(event.bubbles, true) 393 | }) 394 | 395 | it("should be readonly", () => { 396 | const event = new Event("foo") 397 | assert.throws(() => { 398 | // @ts-expect-error 399 | event.bubbles = true 400 | }) 401 | }) 402 | }) 403 | 404 | describe("'cancelable' property", () => { 405 | it("should be false if the constructor option was not present", () => { 406 | const event = new Event("foo") 407 | assert.strictEqual(event.cancelable, false) 408 | }) 409 | 410 | it("should be false if the constructor option was false", () => { 411 | const event = new Event("foo", { cancelable: false }) 412 | assert.strictEqual(event.cancelable, false) 413 | }) 414 | 415 | it("should be true if the constructor option was true", () => { 416 | const event = new Event("foo", { cancelable: true }) 417 | assert.strictEqual(event.cancelable, true) 418 | }) 419 | 420 | it("should be readonly", () => { 421 | const event = new Event("foo") 422 | assert.throws(() => { 423 | // @ts-expect-error 424 | event.cancelable = true 425 | }) 426 | }) 427 | }) 428 | 429 | describe("'returnValue' property", () => { 430 | it("should be true", () => { 431 | const event = new Event("foo") 432 | assert.strictEqual(event.returnValue, true) 433 | }) 434 | 435 | it("should be true after 'preventDefault' method was called if 'cancelable' is false", () => { 436 | const event = new Event("foo") 437 | event.preventDefault() 438 | assert.strictEqual(event.returnValue, true) 439 | assertWarning(NonCancelableEventWasCanceled) 440 | }) 441 | 442 | it("should be false after 'preventDefault' method was called if 'cancelable' is true", () => { 443 | const event = new Event("foo", { cancelable: true }) 444 | event.preventDefault() 445 | assert.strictEqual(event.returnValue, false) 446 | }) 447 | 448 | it("should NOT be changed by assignment if 'cancelable' is false", () => { 449 | const event = new Event("foo") 450 | event.returnValue = false 451 | assert.strictEqual(event.returnValue, true) 452 | assertWarning(NonCancelableEventWasCanceled) 453 | }) 454 | 455 | it("should be changed by assignment if 'cancelable' is true", () => { 456 | const event = new Event("foo", { cancelable: true }) 457 | event.returnValue = false 458 | assert.strictEqual(event.returnValue, false) 459 | }) 460 | 461 | it("should NOT be changed by the assignment of true after 'preventDefault' method was called", () => { 462 | const event = new Event("foo", { cancelable: true }) 463 | event.preventDefault() 464 | event.returnValue = true 465 | assert.strictEqual(event.returnValue, false) 466 | assertWarning(TruthyWasAssignedToReturnValue) 467 | }) 468 | 469 | it("should NOT be changed by the assignment of true after the assginment of false", () => { 470 | const event = new Event("foo", { cancelable: true }) 471 | event.returnValue = false 472 | event.returnValue = true 473 | assert.strictEqual(event.returnValue, false) 474 | assertWarning(TruthyWasAssignedToReturnValue) 475 | }) 476 | }) 477 | 478 | describe("'preventDefault' method", () => { 479 | it("should return undefined", () => { 480 | const event = new Event("foo", { cancelable: true }) 481 | assert.strictEqual(event.preventDefault(), undefined) 482 | }) 483 | 484 | it("should return undefined", () => { 485 | const event = new Event("foo") 486 | assert.strictEqual(event.preventDefault(), undefined) 487 | assertWarning(NonCancelableEventWasCanceled) 488 | }) 489 | }) 490 | 491 | describe("'defaultPrevented' property", () => { 492 | it("should be false", () => { 493 | const event = new Event("foo") 494 | assert.strictEqual(event.defaultPrevented, false) 495 | }) 496 | 497 | it("should be false after 'preventDefault' method was called if 'cancelable' is false", () => { 498 | const event = new Event("foo") 499 | event.preventDefault() 500 | assert.strictEqual(event.defaultPrevented, false) 501 | assertWarning(NonCancelableEventWasCanceled) 502 | }) 503 | 504 | it("should be false after 'preventDefault' method was called if 'cancelable' is true", () => { 505 | const event = new Event("foo", { cancelable: true }) 506 | event.preventDefault() 507 | assert.strictEqual(event.defaultPrevented, true) 508 | }) 509 | 510 | it("should be readonly", () => { 511 | const event = new Event("foo") 512 | assert.throws(() => { 513 | // @ts-expect-error 514 | event.defaultPrevented = true 515 | }) 516 | }) 517 | }) 518 | 519 | describe("'composed' property", () => { 520 | it("should be false if the constructor option was not present", () => { 521 | const event = new Event("foo") 522 | assert.strictEqual(event.composed, false) 523 | }) 524 | 525 | it("should be false if the constructor option was false", () => { 526 | const event = new Event("foo", { composed: false }) 527 | assert.strictEqual(event.composed, false) 528 | }) 529 | 530 | it("should be true if the constructor option was true", () => { 531 | const event = new Event("foo", { composed: true }) 532 | assert.strictEqual(event.composed, true) 533 | }) 534 | 535 | it("should be readonly", () => { 536 | const event = new Event("foo") 537 | assert.throws(() => { 538 | // @ts-expect-error 539 | event.composed = true 540 | }) 541 | }) 542 | }) 543 | 544 | describe("'isTrusted' property", () => { 545 | it("should be false", () => { 546 | const event = new Event("foo") 547 | assert.strictEqual(event.isTrusted, false) 548 | }) 549 | 550 | it("should be readonly", () => { 551 | const event = new Event("foo") 552 | assert.throws(() => { 553 | // @ts-expect-error 554 | event.isTrusted = true 555 | }) 556 | }) 557 | 558 | it("should NOT be configurable", () => { 559 | const event = new Event("foo") 560 | assert.throws(() => { 561 | Object.defineProperty(event, "isTrusted", { value: true }) 562 | }) 563 | }) 564 | 565 | it("should NOT be overridable", () => { 566 | class CustomEvent extends Event { 567 | // eslint-disable-next-line class-methods-use-this 568 | public get isTrusted(): boolean { 569 | return true 570 | } 571 | } 572 | const event = new CustomEvent("foo") 573 | assert.strictEqual(event.isTrusted, false) 574 | }) 575 | }) 576 | 577 | describe("'timeStamp' property", () => { 578 | it("should be a number", () => { 579 | const event = new Event("foo") 580 | assert.strictEqual(typeof event.timeStamp, "number") 581 | }) 582 | 583 | it("should be readonly", () => { 584 | const event = new Event("foo") 585 | assert.throws(() => { 586 | // @ts-expect-error 587 | event.timeStamp = 0 588 | }) 589 | }) 590 | }) 591 | 592 | describe("'initEvent' method", () => { 593 | it("should return undefined", () => { 594 | const event = new Event("foo") 595 | assert.strictEqual(event.initEvent("bar"), undefined) 596 | }) 597 | 598 | it("should set type", () => { 599 | const event = new Event("foo") 600 | event.initEvent("bar") 601 | assert.strictEqual(event.type, "bar") 602 | }) 603 | 604 | it("should set type (string representation)", () => { 605 | const event = new Event("foo") 606 | // @ts-expect-error 607 | event.initEvent(1e3) 608 | assert.strictEqual(event.type, "1000") 609 | }) 610 | 611 | it("should set bubbles", () => { 612 | const event = new Event("foo") 613 | event.initEvent("foo", true) 614 | assert.strictEqual(event.bubbles, true) 615 | assert.strictEqual(event.cancelable, false) 616 | assert.strictEqual(event.composed, false) 617 | }) 618 | 619 | it("should set cancelable", () => { 620 | const event = new Event("foo", { bubbles: true }) 621 | event.initEvent("foo", undefined, true) 622 | assert.strictEqual(event.bubbles, false) 623 | assert.strictEqual(event.cancelable, true) 624 | assert.strictEqual(event.composed, false) 625 | }) 626 | 627 | it("should not change composed", () => { 628 | const event = new Event("foo", { 629 | bubbles: true, 630 | cancelable: true, 631 | composed: true, 632 | }) 633 | event.initEvent("foo") 634 | assert.strictEqual(event.bubbles, false) 635 | assert.strictEqual(event.cancelable, false) 636 | assert.strictEqual(event.composed, true) 637 | }) 638 | 639 | it("should reset 'stopPropagation' flag", () => { 640 | const event = new Event("foo") 641 | event.stopPropagation() 642 | assert.strictEqual(event.cancelBubble, true) 643 | event.initEvent("foo") 644 | assert.strictEqual(event.cancelBubble, false) 645 | }) 646 | 647 | it("should reset 'canceled' flag", () => { 648 | const event = new Event("foo", { cancelable: true }) 649 | event.preventDefault() 650 | assert.strictEqual(event.defaultPrevented, true) 651 | event.initEvent("foo") 652 | assert.strictEqual(event.defaultPrevented, false) 653 | }) 654 | 655 | it("should do nothing under dispatching", () => { 656 | const target = new EventTarget() 657 | const event = new Event("foo") 658 | 659 | target.addEventListener("foo", () => { 660 | event.initEvent("bar") 661 | }) 662 | target.dispatchEvent(event) 663 | 664 | assert.strictEqual(event.type, "foo") 665 | assertWarning(InitEventWasCalledWhileDispatching) 666 | }) 667 | }) 668 | 669 | describe("for-in", () => { 670 | it("should enumerate 22 property names", () => { 671 | const event = new Event("foo") 672 | const actualKeys = new Set() 673 | 674 | // eslint-disable-next-line @mysticatea/prefer-for-of 675 | for (const key in event) { 676 | actualKeys.add(key) 677 | } 678 | 679 | for (const expectedKey of [ 680 | "type", 681 | "target", 682 | "srcElement", 683 | "currentTarget", 684 | "composedPath", 685 | "NONE", 686 | "CAPTURING_PHASE", 687 | "AT_TARGET", 688 | "BUBBLING_PHASE", 689 | "eventPhase", 690 | "stopPropagation", 691 | "cancelBubble", 692 | "stopImmediatePropagation", 693 | "bubbles", 694 | "cancelable", 695 | "returnValue", 696 | "preventDefault", 697 | "defaultPrevented", 698 | "composed", 699 | "isTrusted", 700 | "timeStamp", 701 | "initEvent", 702 | ]) { 703 | assert( 704 | actualKeys.has(expectedKey), 705 | `for-in loop should iterate '${expectedKey}' key`, 706 | ) 707 | } 708 | }) 709 | 710 | it("should enumerate 4 property names in static", () => { 711 | const actualKeys = [] 712 | const expectedKeys = [ 713 | "AT_TARGET", 714 | "BUBBLING_PHASE", 715 | "CAPTURING_PHASE", 716 | "NONE", 717 | ] 718 | 719 | // eslint-disable-next-line @mysticatea/prefer-for-of 720 | for (const key in Event) { 721 | actualKeys.push(key) 722 | } 723 | 724 | assert.deepStrictEqual( 725 | actualKeys.sort(undefined), 726 | expectedKeys.sort(undefined), 727 | ) 728 | }) 729 | }) 730 | }) 731 | -------------------------------------------------------------------------------- /test/fixtures/entrypoint.ts: -------------------------------------------------------------------------------- 1 | import Mocha0 from "mocha" 2 | import { Global } from "../../src/lib/global" 3 | 4 | const Mocha = Global.Mocha ?? Mocha0 5 | 6 | export const result = main().then(failures => ({ 7 | failures, 8 | coverage: Global.__coverage__ ?? {}, 9 | })) 10 | Global.result = result 11 | 12 | async function main(): Promise { 13 | const tester = new Mocha({ 14 | allowUncaught: false, 15 | color: true, 16 | fullTrace: true, 17 | reporter: "spec", 18 | }) 19 | 20 | //-------------------------------------------------------------------------- 21 | // Test cases 22 | //-------------------------------------------------------------------------- 23 | 24 | tester.suite.emit("pre-require", Global, "event.ts", tester) 25 | tester.suite.emit("require", await import("../event"), "event.ts", tester) 26 | tester.suite.emit("post-require", Global, "event.ts", tester) 27 | 28 | tester.suite.emit("pre-require", Global, "event-target.ts", tester) 29 | tester.suite.emit( 30 | "require", 31 | await import("../event-target"), 32 | "event-target.ts", 33 | tester, 34 | ) 35 | tester.suite.emit("post-require", Global, "event-target.ts", tester) 36 | 37 | tester.suite.emit("pre-require", Global, "event-attribute.ts", tester) 38 | tester.suite.emit( 39 | "require", 40 | await import("../event-attribute"), 41 | "event-attribute.ts", 42 | tester, 43 | ) 44 | tester.suite.emit("post-require", Global, "event-attribute.ts", tester) 45 | 46 | tester.suite.emit( 47 | "pre-require", 48 | Global, 49 | "define-custom-event-target.ts", 50 | tester, 51 | ) 52 | tester.suite.emit( 53 | "require", 54 | await import("../define-custom-event-target"), 55 | "define-custom-event-target.ts", 56 | tester, 57 | ) 58 | tester.suite.emit( 59 | "post-require", 60 | Global, 61 | "define-custom-event-target.ts", 62 | tester, 63 | ) 64 | 65 | tester.suite.emit("pre-require", Global, "default-error-handler.ts", tester) 66 | tester.suite.emit( 67 | "require", 68 | await import("../default-error-handler"), 69 | "default-error-handler.ts", 70 | tester, 71 | ) 72 | tester.suite.emit( 73 | "post-require", 74 | Global, 75 | "default-error-handler.ts", 76 | tester, 77 | ) 78 | 79 | tester.suite.emit( 80 | "pre-require", 81 | Global, 82 | "default-warning-handler.ts", 83 | tester, 84 | ) 85 | tester.suite.emit( 86 | "require", 87 | await import("../default-warning-handler"), 88 | "default-warning-handler.ts", 89 | tester, 90 | ) 91 | tester.suite.emit( 92 | "post-require", 93 | Global, 94 | "default-warning-handler.ts", 95 | tester, 96 | ) 97 | 98 | //-------------------------------------------------------------------------- 99 | // Run 100 | //-------------------------------------------------------------------------- 101 | 102 | return new Promise(resolve => { 103 | tester.run(resolve) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /test/fixtures/types.ts: -------------------------------------------------------------------------------- 1 | /// 2 | import { 3 | Event as EventShim, 4 | EventTarget as EventTargetShim, 5 | getEventAttributeValue, 6 | setEventAttributeValue, 7 | } from "../../src/index" 8 | 9 | let signal = new AbortController().signal 10 | 11 | class MyEvent extends EventShim<"myevent"> { 12 | readonly value: number 13 | constructor(value: number) { 14 | super("myevent") 15 | this.value = value 16 | } 17 | } 18 | type EventMap1 = { 19 | test: EventShim<"test"> 20 | myevent: MyEvent 21 | } 22 | 23 | const a = new EventTargetShim() 24 | const b = new EventTargetShim() 25 | 26 | //------------------------------------------------------------------------------ 27 | // Assignments 28 | //------------------------------------------------------------------------------ 29 | 30 | let EventDomToShim: EventShim = new Event("test") 31 | let EventShimToDom: Event = new EventShim("test") 32 | let EventTargetDomToShim: EventTargetShim = new EventTarget() 33 | let EventTargetShimToDom: EventTarget = new EventTargetShim() 34 | let EventTargetDomToShim1: EventTargetShim = new EventTarget() 35 | let EventTargetShimToDom1: EventTarget = new EventTargetShim() 36 | let AbortSignalDomToShim: EventTargetShim.AbortSignal = new AbortController() 37 | .signal 38 | let AbortSignalShimToDom: AbortSignal = {} as EventTargetShim.AbortSignal 39 | 40 | //------------------------------------------------------------------------------ 41 | // EventTarget#addEventListener 42 | //------------------------------------------------------------------------------ 43 | 44 | a.addEventListener("test") 45 | a.addEventListener("test", (_event: EventShim) => {}) 46 | a.addEventListener("test", (_event: Event) => {}) 47 | a.addEventListener("test", event => { 48 | const domEvent: Event = event 49 | const shimEvent: EventShim = event 50 | }) 51 | b.addEventListener("test", event => { 52 | // `event` is an `Event` 53 | const ev: Event = event 54 | // @ts-expect-error -- `Event` cannot be assigned to `MyEvent`. 55 | const myEvent: MyEvent = event 56 | // @ts-expect-error -- `Event` cannot be assigned to `string`. 57 | const str: string = event 58 | }) 59 | b.addEventListener("myevent", event => { 60 | // `event` is an `MyEvent` 61 | const ev1: Event = event 62 | const ev2: MyEvent = event 63 | // @ts-expect-error -- `MyEvent` cannot be assigned to `string`. 64 | const str: string = event 65 | }) 66 | b.addEventListener("non-exist", event => { 67 | // `event` is an `Event` 68 | const ev: Event = event 69 | // @ts-expect-error -- `Event` cannot be assigned to `MyEvent`. 70 | const myEvent: MyEvent = event 71 | // @ts-expect-error -- `Event` cannot be assigned to `string`. 72 | const str: string = event 73 | }) 74 | 75 | // Options 76 | a.addEventListener("test", null, true) 77 | a.addEventListener("test", null, { capture: true }) 78 | a.addEventListener("test", null, { once: true }) 79 | a.addEventListener("test", null, { passive: true }) 80 | a.addEventListener("test", null, { signal: signal }) 81 | a.addEventListener("test", null, { 82 | capture: true, 83 | once: true, 84 | passive: true, 85 | signal: signal, 86 | }) 87 | 88 | // @ts-expect-error -- require `type` argument at least. 89 | a.addEventListener() 90 | // @ts-expect-error -- `foo` doesn't exist. 91 | a.addEventListener("test", null, { foo: true }) 92 | 93 | //------------------------------------------------------------------------------ 94 | // EventTarget#removeEventListener 95 | //------------------------------------------------------------------------------ 96 | 97 | a.removeEventListener("test") 98 | a.removeEventListener("test", (_event: EventShim) => {}) 99 | a.removeEventListener("test", (_event: Event) => {}) 100 | 101 | // Options 102 | a.removeEventListener("test", null, true) 103 | a.removeEventListener("test", null, { capture: true }) 104 | 105 | // @ts-expect-error -- require `type` argument at least. 106 | a.removeEventListener() 107 | // @ts-expect-error -- `once` doesn't exist. 108 | a.removeEventListener("test", null, { once: true }) 109 | // @ts-expect-error -- `passive` doesn't exist. 110 | a.removeEventListener("test", null, { passive: true }) 111 | // @ts-expect-error -- `signal` doesn't exist. 112 | a.removeEventListener("test", null, { signal: signal }) 113 | 114 | //------------------------------------------------------------------------------ 115 | // EventTarget#dispatchEvent 116 | //------------------------------------------------------------------------------ 117 | 118 | a.dispatchEvent(new Event("test")) 119 | a.dispatchEvent(new EventShim("test")) 120 | 121 | // @ts-expect-error -- require `event` argument. 122 | a.dispatchEvent() 123 | 124 | //------------------------------------------------------------------------------ 125 | // Strict Mode 126 | //------------------------------------------------------------------------------ 127 | 128 | let my = new EventTargetShim() 129 | my.addEventListener("test", event => { 130 | const test: EventShim<"test"> = event 131 | // @ts-expect-error -- `Event` cannot be assigned to `MyEvent`. 132 | const myevent: MyEvent = event 133 | }) 134 | my.addEventListener("myevent", event => { 135 | // @ts-expect-error -- `MyEvent` cannot be assigned to `Event<"test">`. 136 | const test: EventShim<"test"> = event 137 | const ev: Event = event 138 | const myevent: MyEvent = event 139 | }) 140 | // @ts-expect-error -- non-exist cannot be assgined to `"test" | "myevent"`. 141 | my.addEventListener("non-exist", _event => {}) 142 | my.dispatchEvent(new EventShim("test")) 143 | my.dispatchEvent(new MyEvent(1)) 144 | my.dispatchEvent({ type: "test" }) 145 | my.dispatchEvent({ type: "myevent", value: 1 }) 146 | // @ts-expect-error -- require `value` property 147 | my.dispatchEvent({ type: "myevent" }) 148 | // @ts-expect-error -- `type` must be "test" or "myevent" 149 | my.dispatchEvent({ type: "nonexist" }) 150 | // @ts-expect-error -- `type` must be "test" or "myevent" 151 | my.dispatchEvent(new Event("test")) 152 | 153 | //------------------------------------------------------------------------------ 154 | // getEventAttributeValue / setEventAttributeValue 155 | //------------------------------------------------------------------------------ 156 | 157 | class MyEventTarget1 extends EventTargetShim { 158 | get ontest() { 159 | return getEventAttributeValue(this, "test") 160 | } 161 | set ontest(value) { 162 | setEventAttributeValue(this, "test", value) 163 | } 164 | get onmyevent() { 165 | return getEventAttributeValue( 166 | this, 167 | "myevent", 168 | ) 169 | } 170 | set onmyevent(value) { 171 | setEventAttributeValue(this, "myevent", value) 172 | } 173 | } 174 | let eav = new MyEventTarget1() 175 | eav.ontest = e => { 176 | const shim: EventShim<"test"> = e 177 | // @ts-expect-error -- `e` is EventShim<"test"> 178 | const myevent: MyEvent = e 179 | } 180 | eav.onmyevent = e => { 181 | const shim: MyEvent = e 182 | // @ts-expect-error -- `e` is MyEvent 183 | const myevent: EventShim<"test"> = e 184 | } 185 | -------------------------------------------------------------------------------- /test/lib/abort-signal-stub.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Event, 3 | EventTarget, 4 | getEventAttributeValue, 5 | setEventAttributeValue, 6 | } from "../../src/index" 7 | 8 | type AbortSignalEventMap = { 9 | abort: Event 10 | } 11 | 12 | /** 13 | * Stub for AbortSignal. 14 | */ 15 | export class AbortSignalStub extends EventTarget { 16 | public aborted = false 17 | 18 | public get onabort(): EventTarget.CallbackFunction< 19 | EventTarget.AbortSignal, 20 | Event 21 | > | null { 22 | return getEventAttributeValue( 23 | this, 24 | "abort", 25 | ) 26 | } 27 | public set onabort(value) { 28 | setEventAttributeValue(this, "abort", value) 29 | } 30 | 31 | public abort(): void { 32 | this.aborted = true 33 | this.dispatchEvent(new Event("abort")) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/lib/count-event-listeners.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EventTarget, 3 | getEventTargetInternalData, 4 | } from "../../src/lib/event-target" 5 | 6 | /** 7 | * Get registered event listeners from an `EventTarget` object. 8 | * @param target The `EventTarget` object to get. 9 | * @param type The type of events to get. 10 | */ 11 | export function countEventListeners( 12 | target: EventTarget, 13 | type?: string, 14 | ): number { 15 | const listenerMap = getEventTargetInternalData(target, "target") 16 | const keys = type ? [type] : Object.keys(listenerMap) 17 | return keys.reduce( 18 | (count, key) => count + (listenerMap[key]?.listeners.length ?? 0), 19 | 0, 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /test/lib/setup-error-check.ts: -------------------------------------------------------------------------------- 1 | import assert from "assert" 2 | import { setErrorHandler, setWarningHandler } from "../../src" 3 | import { Warning } from "../../src/lib/warning-handler" 4 | 5 | export function setupErrorCheck() { 6 | const errors: Error[] = [] 7 | const warnings: setWarningHandler.Warning[] = [] 8 | 9 | beforeEach(() => { 10 | errors.length = 0 11 | warnings.length = 0 12 | setErrorHandler(error => { 13 | errors.push(error) 14 | }) 15 | setWarningHandler(warning => { 16 | warnings.push(warning) 17 | }) 18 | }) 19 | 20 | afterEach(function () { 21 | setErrorHandler(undefined) 22 | setWarningHandler(undefined) 23 | try { 24 | assert.deepStrictEqual(errors, [], "Errors should be nothing.") 25 | assert.deepStrictEqual(warnings, [], "Warnings should be nothing.") 26 | } catch (error) { 27 | ;(this.test as any)?.error(error) 28 | } 29 | }) 30 | 31 | function assertError(errorOrMessage: Error | string): void { 32 | const actualError = errors.shift() 33 | assert.strictEqual( 34 | typeof errorOrMessage === "string" 35 | ? actualError?.message 36 | : actualError, 37 | errorOrMessage, 38 | ) 39 | } 40 | 41 | function assertWarning( 42 | warning: Warning, 43 | ...args: TArgs 44 | ): void { 45 | const actualWarning = warnings.shift() 46 | assert.deepStrictEqual(actualWarning, { ...warning, args }) 47 | } 48 | 49 | return { assertError, assertWarning } 50 | } 51 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig/base.json", 3 | "compilerOptions": { 4 | "checkJs": true, 5 | "module": "CommonJS", 6 | "noEmit": true, 7 | "noUnusedLocals": false, 8 | "noUnusedParameters": false, 9 | "types": ["mocha", "node"] 10 | }, 11 | "include": ["scripts/**/*.ts", "src/**/*.ts", "test/**/*.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowSyntheticDefaultImports": true, 4 | "esModuleInterop": true, 5 | "inlineSources": true, 6 | "lib": ["ES2020"], 7 | "module": "ES2020", 8 | "moduleResolution": "Node", 9 | "newLine": "lf", 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "noUnusedLocals": true, 13 | "noUnusedParameters": true, 14 | "skipLibCheck": true, 15 | "sourceMap": true, 16 | "strict": true, 17 | "target": "ES2018", 18 | "types": [] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tsconfig/build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": {}, 4 | "include": ["../src/**/*.ts"] 5 | } 6 | -------------------------------------------------------------------------------- /tsconfig/dts.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2020", "DOM"], 5 | "skipLibCheck": false 6 | }, 7 | "include": ["../src/**/*.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./base.json", 3 | "compilerOptions": { 4 | "lib": ["ES2020", "DOM"], 5 | "types": ["mocha"] 6 | }, 7 | "include": ["../src/**/*.ts", "../test/**/*.ts"], 8 | "exclude": ["../test/fixtures/types.ts"] 9 | } 10 | --------------------------------------------------------------------------------