events: add CustomEvent
This implements the Web API `CustomEvent` in `internal/event_target`. Signed-off-by: Daeyeon Jeong daeyeon.dev@gmail.com PR-URL: https://github.com/nodejs/node/pull/43514 Refs: https://github.com/nodejs/node/issues/40678 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Benjamin Gruenbaum <benjamingr@gmail.com>
This commit is contained in:
parent
4eb8e3fb58
commit
cbbd91d804
@ -1987,6 +1987,31 @@ added: v14.5.0
|
|||||||
|
|
||||||
Removes the `listener` from the list of handlers for event `type`.
|
Removes the `listener` from the list of handlers for event `type`.
|
||||||
|
|
||||||
|
### Class: `CustomEvent`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental.
|
||||||
|
|
||||||
|
* Extends: {Event}
|
||||||
|
|
||||||
|
The `CustomEvent` object is an adaptation of the [`CustomEvent` Web API][].
|
||||||
|
Instances are created internally by Node.js.
|
||||||
|
|
||||||
|
#### `event.detail`
|
||||||
|
|
||||||
|
<!-- YAML
|
||||||
|
added: REPLACEME
|
||||||
|
-->
|
||||||
|
|
||||||
|
> Stability: 1 - Experimental.
|
||||||
|
|
||||||
|
* Type: {any} Returns custom data passed when initializing.
|
||||||
|
|
||||||
|
Read-only.
|
||||||
|
|
||||||
### Class: `NodeEventTarget`
|
### Class: `NodeEventTarget`
|
||||||
|
|
||||||
<!-- YAML
|
<!-- YAML
|
||||||
@ -2124,6 +2149,7 @@ to the `EventTarget`.
|
|||||||
|
|
||||||
[WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget
|
[WHATWG-EventTarget]: https://dom.spec.whatwg.org/#interface-eventtarget
|
||||||
[`--trace-warnings`]: cli.md#--trace-warnings
|
[`--trace-warnings`]: cli.md#--trace-warnings
|
||||||
|
[`CustomEvent` Web API]: https://dom.spec.whatwg.org/#customevent
|
||||||
[`EventTarget` Web API]: https://dom.spec.whatwg.org/#eventtarget
|
[`EventTarget` Web API]: https://dom.spec.whatwg.org/#eventtarget
|
||||||
[`EventTarget` error handling]: #eventtarget-error-handling
|
[`EventTarget` error handling]: #eventtarget-error-handling
|
||||||
[`Event` Web API]: https://dom.spec.whatwg.org/#event
|
[`Event` Web API]: https://dom.spec.whatwg.org/#event
|
||||||
|
@ -67,6 +67,7 @@ const kTrustEvent = Symbol('kTrustEvent');
|
|||||||
const { now } = require('internal/perf/utils');
|
const { now } = require('internal/perf/utils');
|
||||||
|
|
||||||
const kType = Symbol('type');
|
const kType = Symbol('type');
|
||||||
|
const kDetail = Symbol('detail');
|
||||||
|
|
||||||
const isTrustedSet = new SafeWeakSet();
|
const isTrustedSet = new SafeWeakSet();
|
||||||
const isTrusted = ObjectGetOwnPropertyDescriptor({
|
const isTrusted = ObjectGetOwnPropertyDescriptor({
|
||||||
@ -322,6 +323,49 @@ ObjectDefineProperties(
|
|||||||
stopPropagation: kEnumerableProperty,
|
stopPropagation: kEnumerableProperty,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function isCustomEvent(value) {
|
||||||
|
return isEvent(value) && (value?.[kDetail] !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomEvent extends Event {
|
||||||
|
/**
|
||||||
|
* @constructor
|
||||||
|
* @param {string} type
|
||||||
|
* @param {{
|
||||||
|
* bubbles?: boolean,
|
||||||
|
* cancelable?: boolean,
|
||||||
|
* composed?: boolean,
|
||||||
|
* detail?: any,
|
||||||
|
* }} [options]
|
||||||
|
*/
|
||||||
|
constructor(type, options = kEmptyObject) {
|
||||||
|
if (arguments.length === 0)
|
||||||
|
throw new ERR_MISSING_ARGS('type');
|
||||||
|
super(type, options);
|
||||||
|
this[kDetail] = options?.detail ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {any}
|
||||||
|
*/
|
||||||
|
get detail() {
|
||||||
|
if (!isCustomEvent(this))
|
||||||
|
throw new ERR_INVALID_THIS('CustomEvent');
|
||||||
|
return this[kDetail];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectDefineProperties(CustomEvent.prototype, {
|
||||||
|
[SymbolToStringTag]: {
|
||||||
|
__proto__: null,
|
||||||
|
writable: false,
|
||||||
|
enumerable: false,
|
||||||
|
configurable: true,
|
||||||
|
value: 'CustomEvent',
|
||||||
|
},
|
||||||
|
detail: kEnumerableProperty,
|
||||||
|
});
|
||||||
|
|
||||||
class NodeCustomEvent extends Event {
|
class NodeCustomEvent extends Event {
|
||||||
constructor(type, options) {
|
constructor(type, options) {
|
||||||
super(type, options);
|
super(type, options);
|
||||||
@ -984,6 +1028,7 @@ const EventEmitterMixin = (Superclass) => {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
Event,
|
Event,
|
||||||
|
CustomEvent,
|
||||||
EventEmitterMixin,
|
EventEmitterMixin,
|
||||||
EventTarget,
|
EventTarget,
|
||||||
NodeEventTarget,
|
NodeEventTarget,
|
||||||
|
325
test/parallel/test-events-customevent.js
Normal file
325
test/parallel/test-events-customevent.js
Normal file
@ -0,0 +1,325 @@
|
|||||||
|
// Flags: --expose-internals
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const common = require('../common');
|
||||||
|
const { ok, strictEqual, deepStrictEqual, throws } = require('node:assert');
|
||||||
|
const { inspect } = require('node:util');
|
||||||
|
const { Event, EventTarget, CustomEvent } = require('internal/event_target');
|
||||||
|
|
||||||
|
{
|
||||||
|
ok(CustomEvent);
|
||||||
|
|
||||||
|
// Default string
|
||||||
|
const tag = Object.prototype.toString.call(new CustomEvent('$'));
|
||||||
|
strictEqual(tag, '[object CustomEvent]');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// No argument behavior - throw TypeError
|
||||||
|
throws(() => {
|
||||||
|
new CustomEvent();
|
||||||
|
}, TypeError);
|
||||||
|
|
||||||
|
throws(() => new CustomEvent(Symbol()), TypeError);
|
||||||
|
|
||||||
|
// Too many arguments passed behavior - ignore additional arguments
|
||||||
|
const ev = new CustomEvent('foo', {}, {});
|
||||||
|
strictEqual(ev.type, 'foo');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('$');
|
||||||
|
strictEqual(ev.type, '$');
|
||||||
|
strictEqual(ev.bubbles, false);
|
||||||
|
strictEqual(ev.cancelable, false);
|
||||||
|
strictEqual(ev.detail, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Coercion to string works
|
||||||
|
strictEqual(new CustomEvent(1).type, '1');
|
||||||
|
strictEqual(new CustomEvent(false).type, 'false');
|
||||||
|
strictEqual(new CustomEvent({}).type, String({}));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('$', {
|
||||||
|
detail: 56,
|
||||||
|
sweet: 'x',
|
||||||
|
cancelable: true,
|
||||||
|
});
|
||||||
|
strictEqual(ev.type, '$');
|
||||||
|
strictEqual(ev.bubbles, false);
|
||||||
|
strictEqual(ev.cancelable, true);
|
||||||
|
strictEqual(ev.sweet, undefined);
|
||||||
|
strictEqual(ev.detail, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Any types of value for `detail` are acceptable.
|
||||||
|
['foo', 1, false, [], {}].forEach((i) => {
|
||||||
|
const ev = new CustomEvent('$', { detail: i });
|
||||||
|
strictEqual(ev.detail, i);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Readonly `detail` behavior
|
||||||
|
const ev = new CustomEvent('$', {
|
||||||
|
detail: 56,
|
||||||
|
});
|
||||||
|
strictEqual(ev.detail, 56);
|
||||||
|
try {
|
||||||
|
ev.detail = 96;
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
} catch (error) {
|
||||||
|
common.mustCall()();
|
||||||
|
}
|
||||||
|
strictEqual(ev.detail, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new Event('$', {
|
||||||
|
detail: 96,
|
||||||
|
});
|
||||||
|
strictEqual(ev.detail, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following tests verify whether CustomEvent works the same as Event
|
||||||
|
// except carrying custom data. They're based on `parallel/test-eventtarget.js`.
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('$');
|
||||||
|
strictEqual(ev.type, '$');
|
||||||
|
strictEqual(ev.bubbles, false);
|
||||||
|
strictEqual(ev.cancelable, false);
|
||||||
|
strictEqual(ev.detail, null);
|
||||||
|
|
||||||
|
strictEqual(ev.defaultPrevented, false);
|
||||||
|
strictEqual(typeof ev.timeStamp, 'number');
|
||||||
|
|
||||||
|
// Compatibility properties with the DOM
|
||||||
|
deepStrictEqual(ev.composedPath(), []);
|
||||||
|
strictEqual(ev.returnValue, true);
|
||||||
|
strictEqual(ev.composed, false);
|
||||||
|
strictEqual(ev.isTrusted, false);
|
||||||
|
strictEqual(ev.eventPhase, 0);
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
|
||||||
|
// Not cancelable
|
||||||
|
ev.preventDefault();
|
||||||
|
strictEqual(ev.defaultPrevented, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Invalid options
|
||||||
|
['foo', 1, false].forEach((i) =>
|
||||||
|
throws(() => new CustomEvent('foo', i), {
|
||||||
|
code: 'ERR_INVALID_ARG_TYPE',
|
||||||
|
name: 'TypeError',
|
||||||
|
message:
|
||||||
|
'The "options" argument must be of type object.' +
|
||||||
|
common.invalidArgTypeHelper(i),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('$');
|
||||||
|
strictEqual(ev.constructor.name, 'CustomEvent');
|
||||||
|
|
||||||
|
// CustomEvent Statics
|
||||||
|
strictEqual(CustomEvent.NONE, 0);
|
||||||
|
strictEqual(CustomEvent.CAPTURING_PHASE, 1);
|
||||||
|
strictEqual(CustomEvent.AT_TARGET, 2);
|
||||||
|
strictEqual(CustomEvent.BUBBLING_PHASE, 3);
|
||||||
|
strictEqual(new CustomEvent('foo').eventPhase, CustomEvent.NONE);
|
||||||
|
|
||||||
|
// CustomEvent is a function
|
||||||
|
strictEqual(CustomEvent.length, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.cancelBubble = true;
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.stopPropagation();
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.cancelBubble = 'some-truthy-value';
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.cancelBubble = true;
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.stopPropagation();
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
strictEqual(ev.cancelBubble, false);
|
||||||
|
ev.cancelBubble = 'some-truthy-value';
|
||||||
|
strictEqual(ev.cancelBubble, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo', { cancelable: true });
|
||||||
|
strictEqual(ev.type, 'foo');
|
||||||
|
strictEqual(ev.cancelable, true);
|
||||||
|
strictEqual(ev.defaultPrevented, false);
|
||||||
|
|
||||||
|
ev.preventDefault();
|
||||||
|
strictEqual(ev.defaultPrevented, true);
|
||||||
|
}
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('foo');
|
||||||
|
deepStrictEqual(Object.keys(ev), ['isTrusted']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Works with EventTarget
|
||||||
|
|
||||||
|
{
|
||||||
|
const obj = { sweet: 'x', memory: { x: 56, y: 96 } };
|
||||||
|
const et = new EventTarget();
|
||||||
|
const ev = new CustomEvent('$', { detail: obj });
|
||||||
|
const fn = common.mustCall((event) => {
|
||||||
|
strictEqual(event, ev);
|
||||||
|
deepStrictEqual(event.detail, obj);
|
||||||
|
});
|
||||||
|
et.addEventListener('$', fn);
|
||||||
|
et.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const eventTarget = new EventTarget();
|
||||||
|
const event = new CustomEvent('$');
|
||||||
|
eventTarget.dispatchEvent(event);
|
||||||
|
strictEqual(event.target, eventTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const obj = { sweet: 'x' };
|
||||||
|
const eventTarget = new EventTarget();
|
||||||
|
|
||||||
|
const ev1 = common.mustCall(function(event) {
|
||||||
|
strictEqual(event.type, 'foo');
|
||||||
|
strictEqual(event.detail, obj);
|
||||||
|
strictEqual(this, eventTarget);
|
||||||
|
strictEqual(event.eventPhase, 2);
|
||||||
|
}, 2);
|
||||||
|
|
||||||
|
const ev2 = {
|
||||||
|
handleEvent: common.mustCall(function(event) {
|
||||||
|
strictEqual(event.type, 'foo');
|
||||||
|
strictEqual(event.detail, obj);
|
||||||
|
strictEqual(this, ev2);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
eventTarget.addEventListener('foo', ev1);
|
||||||
|
eventTarget.addEventListener('foo', ev2, { once: true });
|
||||||
|
ok(eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj })));
|
||||||
|
eventTarget.dispatchEvent(new CustomEvent('foo', { detail: obj }));
|
||||||
|
|
||||||
|
eventTarget.removeEventListener('foo', ev1);
|
||||||
|
eventTarget.dispatchEvent(new CustomEvent('foo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Same event dispatched multiple times.
|
||||||
|
const obj = { sweet: 'x' };
|
||||||
|
const event = new CustomEvent('foo', { detail: obj });
|
||||||
|
const eventTarget1 = new EventTarget();
|
||||||
|
const eventTarget2 = new EventTarget();
|
||||||
|
|
||||||
|
eventTarget1.addEventListener(
|
||||||
|
'foo',
|
||||||
|
common.mustCall((event) => {
|
||||||
|
strictEqual(event.eventPhase, CustomEvent.AT_TARGET);
|
||||||
|
strictEqual(event.target, eventTarget1);
|
||||||
|
strictEqual(event.detail, obj);
|
||||||
|
deepStrictEqual(event.composedPath(), [eventTarget1]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget2.addEventListener(
|
||||||
|
'foo',
|
||||||
|
common.mustCall((event) => {
|
||||||
|
strictEqual(event.eventPhase, CustomEvent.AT_TARGET);
|
||||||
|
strictEqual(event.target, eventTarget2);
|
||||||
|
strictEqual(event.detail, obj);
|
||||||
|
deepStrictEqual(event.composedPath(), [eventTarget2]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
eventTarget1.dispatchEvent(event);
|
||||||
|
strictEqual(event.eventPhase, CustomEvent.NONE);
|
||||||
|
strictEqual(event.target, eventTarget1);
|
||||||
|
deepStrictEqual(event.composedPath(), []);
|
||||||
|
|
||||||
|
eventTarget2.dispatchEvent(event);
|
||||||
|
strictEqual(event.eventPhase, CustomEvent.NONE);
|
||||||
|
strictEqual(event.target, eventTarget2);
|
||||||
|
deepStrictEqual(event.composedPath(), []);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const obj = { sweet: 'x' };
|
||||||
|
const target = new EventTarget();
|
||||||
|
const event = new CustomEvent('foo', { detail: obj });
|
||||||
|
|
||||||
|
strictEqual(event.target, null);
|
||||||
|
|
||||||
|
target.addEventListener(
|
||||||
|
'foo',
|
||||||
|
common.mustCall((event) => {
|
||||||
|
strictEqual(event.target, target);
|
||||||
|
strictEqual(event.currentTarget, target);
|
||||||
|
strictEqual(event.srcElement, target);
|
||||||
|
strictEqual(event.detail, obj);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
target.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
// Event subclassing
|
||||||
|
const SubEvent = class extends CustomEvent {};
|
||||||
|
const ev = new SubEvent('foo', { detail: 56 });
|
||||||
|
const eventTarget = new EventTarget();
|
||||||
|
const fn = common.mustCall((event) => {
|
||||||
|
strictEqual(event, ev);
|
||||||
|
strictEqual(event.detail, 56);
|
||||||
|
});
|
||||||
|
eventTarget.addEventListener('foo', fn, { once: true });
|
||||||
|
eventTarget.dispatchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Works with inspect
|
||||||
|
|
||||||
|
{
|
||||||
|
const ev = new CustomEvent('test');
|
||||||
|
const evConstructorName = inspect(ev, {
|
||||||
|
depth: -1,
|
||||||
|
});
|
||||||
|
strictEqual(evConstructorName, 'CustomEvent');
|
||||||
|
|
||||||
|
const inspectResult = inspect(ev, {
|
||||||
|
depth: 1,
|
||||||
|
});
|
||||||
|
ok(inspectResult.includes('CustomEvent'));
|
||||||
|
}
|
@ -6,6 +6,7 @@ const assert = require('assert');
|
|||||||
|
|
||||||
const {
|
const {
|
||||||
Event,
|
Event,
|
||||||
|
CustomEvent,
|
||||||
EventTarget,
|
EventTarget,
|
||||||
NodeEventTarget,
|
NodeEventTarget,
|
||||||
} = require('internal/event_target');
|
} = require('internal/event_target');
|
||||||
@ -41,10 +42,37 @@ const {
|
|||||||
});
|
});
|
||||||
|
|
||||||
[
|
[
|
||||||
'addEventListener',
|
'target',
|
||||||
'removeEventListener',
|
'currentTarget',
|
||||||
'dispatchEvent',
|
'srcElement',
|
||||||
|
'type',
|
||||||
|
'cancelable',
|
||||||
|
'defaultPrevented',
|
||||||
|
'timeStamp',
|
||||||
|
'returnValue',
|
||||||
|
'bubbles',
|
||||||
|
'composed',
|
||||||
|
'eventPhase',
|
||||||
|
'detail',
|
||||||
].forEach((i) => {
|
].forEach((i) => {
|
||||||
|
assert.throws(() => Reflect.get(CustomEvent.prototype, i, {}), {
|
||||||
|
code: 'ERR_INVALID_THIS',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
[
|
||||||
|
'stopImmediatePropagation',
|
||||||
|
'preventDefault',
|
||||||
|
'composedPath',
|
||||||
|
'cancelBubble',
|
||||||
|
'stopPropagation',
|
||||||
|
].forEach((i) => {
|
||||||
|
assert.throws(() => Reflect.apply(CustomEvent.prototype[i], [], {}), {
|
||||||
|
code: 'ERR_INVALID_THIS',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
['addEventListener', 'removeEventListener', 'dispatchEvent'].forEach((i) => {
|
||||||
assert.throws(() => Reflect.apply(EventTarget.prototype[i], [], {}), {
|
assert.throws(() => Reflect.apply(EventTarget.prototype[i], [], {}), {
|
||||||
code: 'ERR_INVALID_THIS',
|
code: 'ERR_INVALID_THIS',
|
||||||
});
|
});
|
||||||
|
@ -117,6 +117,7 @@ const customTypesMap = {
|
|||||||
'EventEmitter': 'events.html#class-eventemitter',
|
'EventEmitter': 'events.html#class-eventemitter',
|
||||||
'EventTarget': 'events.html#class-eventtarget',
|
'EventTarget': 'events.html#class-eventtarget',
|
||||||
'Event': 'events.html#class-event',
|
'Event': 'events.html#class-event',
|
||||||
|
'CustomEvent': 'events.html#class-customevent',
|
||||||
'EventListener': 'events.html#event-listener',
|
'EventListener': 'events.html#event-listener',
|
||||||
|
|
||||||
'FileHandle': 'fs.html#class-filehandle',
|
'FileHandle': 'fs.html#class-filehandle',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user