UNPKG

21.3 kBJavaScriptView Raw
1/**
2 * @author Toru Nagashima <https://github.com/mysticatea>
3 * @copyright 2017 Toru Nagashima. All rights reserved.
4 * See LICENSE file in root directory for full license.
5 */
6/**
7 * @typedef {object} PrivateData
8 * @property {EventTarget} eventTarget The event target.
9 * @property {{type:string}} event The original event object.
10 * @property {number} eventPhase The current event phase.
11 * @property {EventTarget|null} currentTarget The current event target.
12 * @property {boolean} canceled The flag to prevent default.
13 * @property {boolean} stopped The flag to stop propagation immediately.
14 * @property {Function|null} passiveListener The listener if the current listener is passive. Otherwise this is null.
15 * @property {number} timeStamp The unix time.
16 * @private
17 */
18
19/**
20 * Private data for event wrappers.
21 * @type {WeakMap<Event, PrivateData>}
22 * @private
23 */
24const privateData = new WeakMap();
25
26/**
27 * Cache for wrapper classes.
28 * @type {WeakMap<Object, Function>}
29 * @private
30 */
31const wrappers = new WeakMap();
32
33/**
34 * Get private data.
35 * @param {Event} event The event object to get private data.
36 * @returns {PrivateData} The private data of the event.
37 * @private
38 */
39function pd(event) {
40 const retv = privateData.get(event);
41 console.assert(retv != null, "'this' is expected an Event object, but got", event);
42 return retv
43}
44
45/**
46 * @see https://dom.spec.whatwg.org/#interface-event
47 * @private
48 */
49/**
50 * The event wrapper.
51 * @constructor
52 * @param {EventTarget} eventTarget The event target of this dispatching.
53 * @param {Event|{type:string}} event The original event to wrap.
54 */
55function Event(eventTarget, event) {
56 privateData.set(this, {
57 eventTarget,
58 event,
59 eventPhase: 2,
60 currentTarget: eventTarget,
61 canceled: false,
62 stopped: false,
63 passiveListener: null,
64 timeStamp: event.timeStamp || Date.now(),
65 });
66
67 // https://heycam.github.io/webidl/#Unforgeable
68 Object.defineProperty(this, "isTrusted", { value: false, enumerable: true });
69
70 // Define accessors
71 const keys = Object.keys(event);
72 for (let i = 0; i < keys.length; ++i) {
73 const key = keys[i];
74 if (!(key in this)) {
75 Object.defineProperty(this, key, defineRedirectDescriptor(key));
76 }
77 }
78}
79
80// Should be enumerable, but class methods are not enumerable.
81Event.prototype = {
82 /**
83 * The type of this event.
84 * @type {string}
85 */
86 get type() {
87 return pd(this).event.type
88 },
89
90 /**
91 * The target of this event.
92 * @type {EventTarget}
93 */
94 get target() {
95 return pd(this).eventTarget
96 },
97
98 /**
99 * The target of this event.
100 * @type {EventTarget}
101 */
102 get currentTarget() {
103 return pd(this).currentTarget
104 },
105
106 /**
107 * @returns {EventTarget[]} The composed path of this event.
108 */
109 composedPath() {
110 const currentTarget = pd(this).currentTarget;
111 if (currentTarget == null) {
112 return []
113 }
114 return [currentTarget]
115 },
116
117 /**
118 * Constant of NONE.
119 * @type {number}
120 */
121 get NONE() {
122 return 0
123 },
124
125 /**
126 * Constant of CAPTURING_PHASE.
127 * @type {number}
128 */
129 get CAPTURING_PHASE() {
130 return 1
131 },
132
133 /**
134 * Constant of AT_TARGET.
135 * @type {number}
136 */
137 get AT_TARGET() {
138 return 2
139 },
140
141 /**
142 * Constant of BUBBLING_PHASE.
143 * @type {number}
144 */
145 get BUBBLING_PHASE() {
146 return 3
147 },
148
149 /**
150 * The target of this event.
151 * @type {number}
152 */
153 get eventPhase() {
154 return pd(this).eventPhase
155 },
156
157 /**
158 * Stop event bubbling.
159 * @returns {void}
160 */
161 stopPropagation() {
162 const data = pd(this);
163 if (typeof data.event.stopPropagation === "function") {
164 data.event.stopPropagation();
165 }
166 },
167
168 /**
169 * Stop event bubbling.
170 * @returns {void}
171 */
172 stopImmediatePropagation() {
173 const data = pd(this);
174
175 data.stopped = true;
176 if (typeof data.event.stopImmediatePropagation === "function") {
177 data.event.stopImmediatePropagation();
178 }
179 },
180
181 /**
182 * The flag to be bubbling.
183 * @type {boolean}
184 */
185 get bubbles() {
186 return Boolean(pd(this).event.bubbles)
187 },
188
189 /**
190 * The flag to be cancelable.
191 * @type {boolean}
192 */
193 get cancelable() {
194 return Boolean(pd(this).event.cancelable)
195 },
196
197 /**
198 * Cancel this event.
199 * @returns {void}
200 */
201 preventDefault() {
202 const data = pd(this);
203 if (data.passiveListener != null) {
204 console.warn("Event#preventDefault() was called from a passive listener:", data.passiveListener);
205 return
206 }
207 if (!data.event.cancelable) {
208 return
209 }
210
211 data.canceled = true;
212 if (typeof data.event.preventDefault === "function") {
213 data.event.preventDefault();
214 }
215 },
216
217 /**
218 * The flag to indicate cancellation state.
219 * @type {boolean}
220 */
221 get defaultPrevented() {
222 return pd(this).canceled
223 },
224
225 /**
226 * The flag to be composed.
227 * @type {boolean}
228 */
229 get composed() {
230 return Boolean(pd(this).event.composed)
231 },
232
233 /**
234 * The unix time of this event.
235 * @type {number}
236 */
237 get timeStamp() {
238 return pd(this).timeStamp
239 },
240};
241
242// `constructor` is not enumerable.
243Object.defineProperty(Event.prototype, "constructor", { value: Event, configurable: true, writable: true });
244
245// Ensure `event instanceof window.Event` is `true`.
246if (typeof window !== "undefined" && typeof window.Event !== "undefined") {
247 Object.setPrototypeOf(Event.prototype, window.Event.prototype);
248
249 // Make association for wrappers.
250 wrappers.set(window.Event.prototype, Event);
251}
252
253/**
254 * Get the property descriptor to redirect a given property.
255 * @param {string} key Property name to define property descriptor.
256 * @returns {PropertyDescriptor} The property descriptor to redirect the property.
257 * @private
258 */
259function defineRedirectDescriptor(key) {
260 return {
261 get() {
262 return pd(this).event[key]
263 },
264 set(value) {
265 pd(this).event[key] = value;
266 },
267 configurable: true,
268 enumerable: true,
269 }
270}
271
272/**
273 * Get the property descriptor to call a given method property.
274 * @param {string} key Property name to define property descriptor.
275 * @returns {PropertyDescriptor} The property descriptor to call the method property.
276 * @private
277 */
278function defineCallDescriptor(key) {
279 return {
280 value() {
281 const event = pd(this).event;
282 return event[key].apply(event, arguments)
283 },
284 configurable: true,
285 enumerable: true,
286 }
287}
288
289/**
290 * Define new wrapper class.
291 * @param {Function} BaseEvent The base wrapper class.
292 * @param {Object} proto The prototype of the original event.
293 * @returns {Function} The defined wrapper class.
294 * @private
295 */
296function defineWrapper(BaseEvent, proto) {
297 const keys = Object.keys(proto);
298 if (keys.length === 0) {
299 return BaseEvent
300 }
301
302 /** CustomEvent */
303 function CustomEvent(eventTarget, event) {
304 BaseEvent.call(this, eventTarget, event);
305 }
306
307 CustomEvent.prototype = Object.create(BaseEvent.prototype, {
308 constructor: { value: CustomEvent, configurable: true, writable: true },
309 });
310
311 // Define accessors.
312 for (let i = 0; i < keys.length; ++i) {
313 const key = keys[i];
314 if (!(key in BaseEvent.prototype)) {
315 const descriptor = Object.getOwnPropertyDescriptor(proto, key);
316 const isFunc = (typeof descriptor.value === "function");
317 Object.defineProperty(
318 CustomEvent.prototype,
319 key,
320 isFunc ? defineCallDescriptor(key) : defineRedirectDescriptor(key)
321 );
322 }
323 }
324
325 return CustomEvent
326}
327
328/**
329 * Get the wrapper class of a given prototype.
330 * @param {Object} proto The prototype of the original event to get its wrapper.
331 * @returns {Function} The wrapper class.
332 * @private
333 */
334function getWrapper(proto) {
335 if (proto == null || proto === Object.prototype) {
336 return Event
337 }
338
339 let wrapper = wrappers.get(proto);
340 if (wrapper == null) {
341 wrapper = defineWrapper(getWrapper(Object.getPrototypeOf(proto)), proto);
342 wrappers.set(proto, wrapper);
343 }
344 return wrapper
345}
346
347/**
348 * Wrap a given event to management a dispatching.
349 * @param {EventTarget} eventTarget The event target of this dispatching.
350 * @param {Object} event The event to wrap.
351 * @returns {Event} The wrapper instance.
352 * @private
353 */
354function wrapEvent(eventTarget, event) {
355 const Wrapper = getWrapper(Object.getPrototypeOf(event));
356 return new Wrapper(eventTarget, event)
357}
358
359/**
360 * Get the stopped flag of a given event.
361 * @param {Event} event The event to get.
362 * @returns {boolean} The flag to stop propagation immediately.
363 * @private
364 */
365function isStopped(event) {
366 return pd(event).stopped
367}
368
369/**
370 * Set the current event phase of a given event.
371 * @param {Event} event The event to set current target.
372 * @param {number} eventPhase New event phase.
373 * @returns {void}
374 * @private
375 */
376function setEventPhase(event, eventPhase) {
377 pd(event).eventPhase = eventPhase;
378}
379
380/**
381 * Set the current target of a given event.
382 * @param {Event} event The event to set current target.
383 * @param {EventTarget|null} currentTarget New current target.
384 * @returns {void}
385 * @private
386 */
387function setCurrentTarget(event, currentTarget) {
388 pd(event).currentTarget = currentTarget;
389}
390
391/**
392 * Set a passive listener of a given event.
393 * @param {Event} event The event to set current target.
394 * @param {Function|null} passiveListener New passive listener.
395 * @returns {void}
396 * @private
397 */
398function setPassiveListener(event, passiveListener) {
399 pd(event).passiveListener = passiveListener;
400}
401
402/**
403 * @typedef {object} ListenerNode
404 * @property {Function} listener
405 * @property {1|2|3} listenerType
406 * @property {boolean} passive
407 * @property {boolean} once
408 * @property {ListenerNode|null} next
409 * @private
410 */
411
412/**
413 * @type {WeakMap<object, Map<string, ListenerNode>>}
414 * @private
415 */
416const listenersMap = new WeakMap();
417
418// Listener types
419const CAPTURE = 1;
420const BUBBLE = 2;
421const ATTRIBUTE = 3;
422
423/**
424 * Check whether a given value is an object or not.
425 * @param {any} x The value to check.
426 * @returns {boolean} `true` if the value is an object.
427 */
428function isObject(x) {
429 return x !== null && typeof x === "object" //eslint-disable-line no-restricted-syntax
430}
431
432/**
433 * Get listeners.
434 * @param {EventTarget} eventTarget The event target to get.
435 * @returns {Map<string, ListenerNode>} The listeners.
436 * @private
437 */
438function getListeners(eventTarget) {
439 const listeners = listenersMap.get(eventTarget);
440 console.assert(listeners != null, "'this' is expected an EventTarget object, but got", eventTarget);
441 return listeners || new Map()
442}
443
444/**
445 * Get the property descriptor for the event attribute of a given event.
446 * @param {string} eventName The event name to get property descriptor.
447 * @returns {PropertyDescriptor} The property descriptor.
448 * @private
449 */
450function defineEventAttributeDescriptor(eventName) {
451 return {
452 get() {
453 const listeners = getListeners(this);
454 let node = listeners.get(eventName);
455 while (node != null) {
456 if (node.listenerType === ATTRIBUTE) {
457 return node.listener
458 }
459 node = node.next;
460 }
461 return null
462 },
463
464 set(listener) {
465 if (typeof listener !== "function" && !isObject(listener)) {
466 listener = null; // eslint-disable-line no-param-reassign
467 }
468 const listeners = getListeners(this);
469
470 // Traverse to the tail while removing old value.
471 let prev = null;
472 let node = listeners.get(eventName);
473 while (node != null) {
474 if (node.listenerType === ATTRIBUTE) {
475 // Remove old value.
476 if (prev !== null) {
477 prev.next = node.next;
478 }
479 else if (node.next !== null) {
480 listeners.set(eventName, node.next);
481 }
482 else {
483 listeners.delete(eventName);
484 }
485 }
486 else {
487 prev = node;
488 }
489
490 node = node.next;
491 }
492
493 // Add new value.
494 if (listener !== null) {
495 const newNode = {
496 listener,
497 listenerType: ATTRIBUTE,
498 passive: false,
499 once: false,
500 next: null,
501 };
502 if (prev === null) {
503 listeners.set(eventName, newNode);
504 }
505 else {
506 prev.next = newNode;
507 }
508 }
509 },
510 configurable: true,
511 enumerable: true,
512 }
513}
514
515/**
516 * Define an event attribute (e.g. `eventTarget.onclick`).
517 * @param {Object} eventTargetPrototype The event target prototype to define an event attrbite.
518 * @param {string} eventName The event name to define.
519 * @returns {void}
520 */
521function defineEventAttribute(eventTargetPrototype, eventName) {
522 Object.defineProperty(eventTargetPrototype, `on${eventName}`, defineEventAttributeDescriptor(eventName));
523}
524
525/**
526 * Define a custom EventTarget with event attributes.
527 * @param {string[]} eventNames Event names for event attributes.
528 * @returns {EventTarget} The custom EventTarget.
529 * @private
530 */
531function defineCustomEventTarget(eventNames) {
532 /** CustomEventTarget */
533 function CustomEventTarget() {
534 EventTarget.call(this);
535 }
536
537 CustomEventTarget.prototype = Object.create(EventTarget.prototype, {
538 constructor: { value: CustomEventTarget, configurable: true, writable: true },
539 });
540
541 for (let i = 0; i < eventNames.length; ++i) {
542 defineEventAttribute(CustomEventTarget.prototype, eventNames[i]);
543 }
544
545 return CustomEventTarget
546}
547
548/**
549 * EventTarget.
550 *
551 * - This is constructor if no arguments.
552 * - This is a function which returns a CustomEventTarget constructor if there are arguments.
553 *
554 * For example:
555 *
556 * class A extends EventTarget {}
557 * class B extends EventTarget("message") {}
558 * class C extends EventTarget("message", "error") {}
559 * class D extends EventTarget(["message", "error"]) {}
560 */
561function EventTarget() {
562 /*eslint-disable consistent-return */
563 if (this instanceof EventTarget) {
564 listenersMap.set(this, new Map());
565 return
566 }
567 if (arguments.length === 1 && Array.isArray(arguments[0])) {
568 return defineCustomEventTarget(arguments[0])
569 }
570 if (arguments.length > 0) {
571 const types = new Array(arguments.length);
572 for (let i = 0; i < arguments.length; ++i) {
573 types[i] = arguments[i];
574 }
575 return defineCustomEventTarget(types)
576 }
577 throw new TypeError("Cannot call a class as a function")
578 /*eslint-enable consistent-return */
579}
580
581// Should be enumerable, but class methods are not enumerable.
582EventTarget.prototype = {
583 /**
584 * Add a given listener to this event target.
585 * @param {string} eventName The event name to add.
586 * @param {Function} listener The listener to add.
587 * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener.
588 * @returns {boolean} `true` if the listener was added actually.
589 */
590 addEventListener(eventName, listener, options) {
591 if (listener == null) {
592 return false
593 }
594 if (typeof listener !== "function" && !isObject(listener)) {
595 throw new TypeError("'listener' should be a function or an object.")
596 }
597
598 const listeners = getListeners(this);
599 const optionsIsObj = isObject(options);
600 const capture = optionsIsObj ? Boolean(options.capture) : Boolean(options);
601 const listenerType = (capture ? CAPTURE : BUBBLE);
602 const newNode = {
603 listener,
604 listenerType,
605 passive: optionsIsObj && Boolean(options.passive),
606 once: optionsIsObj && Boolean(options.once),
607 next: null,
608 };
609
610 // Set it as the first node if the first node is null.
611 let node = listeners.get(eventName);
612 if (node === undefined) {
613 listeners.set(eventName, newNode);
614 return true
615 }
616
617 // Traverse to the tail while checking duplication..
618 let prev = null;
619 while (node != null) {
620 if (node.listener === listener && node.listenerType === listenerType) {
621 // Should ignore duplication.
622 return false
623 }
624 prev = node;
625 node = node.next;
626 }
627
628 // Add it.
629 prev.next = newNode;
630 return true
631 },
632
633 /**
634 * Remove a given listener from this event target.
635 * @param {string} eventName The event name to remove.
636 * @param {Function} listener The listener to remove.
637 * @param {boolean|{capture?:boolean,passive?:boolean,once?:boolean}} [options] The options for this listener.
638 * @returns {boolean} `true` if the listener was removed actually.
639 */
640 removeEventListener(eventName, listener, options) {
641 if (listener == null) {
642 return false
643 }
644
645 const listeners = getListeners(this);
646 const capture = isObject(options) ? Boolean(options.capture) : Boolean(options);
647 const listenerType = (capture ? CAPTURE : BUBBLE);
648
649 let prev = null;
650 let node = listeners.get(eventName);
651 while (node != null) {
652 if (node.listener === listener && node.listenerType === listenerType) {
653 if (prev !== null) {
654 prev.next = node.next;
655 }
656 else if (node.next !== null) {
657 listeners.set(eventName, node.next);
658 }
659 else {
660 listeners.delete(eventName);
661 }
662 return true
663 }
664
665 prev = node;
666 node = node.next;
667 }
668
669 return false
670 },
671
672 /**
673 * Dispatch a given event.
674 * @param {Event|{type:string}} event The event to dispatch.
675 * @returns {boolean} `false` if canceled.
676 */
677 dispatchEvent(event) {
678 if (event == null || typeof event.type !== "string") {
679 throw new TypeError("\"event.type\" should be a string.")
680 }
681
682 // If listeners aren't registered, terminate.
683 const listeners = getListeners(this);
684 const eventName = event.type;
685 let node = listeners.get(eventName);
686 if (node == null) {
687 return true
688 }
689
690 // Since we cannot rewrite several properties, so wrap object.
691 const wrappedEvent = wrapEvent(this, event);
692
693 // This doesn't process capturing phase and bubbling phase.
694 // This isn't participating in a tree.
695 let prev = null;
696 while (node != null) {
697 // Remove this listener if it's once
698 if (node.once) {
699 if (prev !== null) {
700 prev.next = node.next;
701 }
702 else if (node.next !== null) {
703 listeners.set(eventName, node.next);
704 }
705 else {
706 listeners.delete(eventName);
707 }
708 }
709 else {
710 prev = node;
711 }
712
713 // Call this listener
714 setPassiveListener(wrappedEvent, (node.passive ? node.listener : null));
715 if (typeof node.listener === "function") {
716 node.listener.call(this, wrappedEvent);
717 }
718 else if (node.listenerType !== ATTRIBUTE && typeof node.listener.handleEvent === "function") {
719 node.listener.handleEvent(wrappedEvent);
720 }
721
722 // Break if `event.stopImmediatePropagation` was called.
723 if (isStopped(wrappedEvent)) {
724 break
725 }
726
727 node = node.next;
728 }
729 setPassiveListener(wrappedEvent, null);
730 setEventPhase(wrappedEvent, 0);
731 setCurrentTarget(wrappedEvent, null);
732
733 return !wrappedEvent.defaultPrevented
734 },
735};
736
737// `constructor` is not enumerable.
738Object.defineProperty(EventTarget.prototype, "constructor", { value: EventTarget, configurable: true, writable: true });
739
740// Ensure `eventTarget instanceof window.EventTarget` is `true`.
741if (typeof window !== "undefined" && typeof window.EventTarget !== "undefined") {
742 Object.setPrototypeOf(EventTarget.prototype, window.EventTarget.prototype);
743}
744
745export { defineEventAttribute, EventTarget };
746export default EventTarget;
747//# sourceMappingURL=event-target-shim.mjs.map