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