# All files / src delegateEvent.ts

98.87% Statements 176/178
90% Branches 18/20
100% Functions 7/7
98.87% Lines 176/178

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    1791x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    202x
    202x
    202x
    202x
    202x
    202x
    16x
    16x
    16x
    16x
    16x
    16x
    202x
    202x
    18x
    6x
    6x
    6x
    12x
    18x

    12x
    12x
    12x
    12x
    202x
    1x
    1x
    1x
    1x
    1x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    379x
    353x
    353x
    379x
    379x
    379x
    379x
    379x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    379x
    379x
    379x
    379x
    379x
    353x
    353x
    379x
    379x
    202x
    202x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    1x
    379x
    379x
    379x
    379x
    379x
    379x
    379x
    6026x
    5840x
    5840x
    177x
    379x
    379x
    379x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    202x
    379x
    379x
    379x
     /** @internal */
    export type DelegatedEventTarget = Window | Document | HTMLElement;

    /**
     * This is a "shared" event handler for the provided `eventType`. The event
     * listener will only be created once, but every single callback will be called
     * when the throttled event is triggered. This means that you will need to make
     * sure to remove the provided callback when it is no longer in use.
     *
     * @internal
     */
    export interface DelegatedEventHandler {
      /**
       * Adds the provided callback to the throttled event listener.
       */
      add(callback: EventListener): void;

      /**
       * Attempts to remove the provided callback from the throttled event listener.
       */
      remove(callback: EventListener): void;
    }

    /** @internal */
    export interface DelegableEvent {
      type: string;
      target: DelegatedEventTarget;
      throttle: boolean;
      handler: DelegatedEventHandler;
      options?: boolean | AddEventListenerOptions;
    }

    const delegatedEvents: DelegableEvent[] = [];

    /**
     * Creates the delegated event handler that will run all the callbacks once an
     * event happens. The callbacks' invocation can also be throttled for event
     * types that trigger rapidly for additional performance.
     *
     * The `<K extends keyof WindowEventMap` is a nice thing I found while looking
     * through the `lib.d.ts` implementation of `addEventListener` that will allow
     * you to get the "correct" event type when using the `add` and `remove`
     * functions once you have created this event handler. Otherwise there'd be ts
     * errors trying to do `MouseEvent` or `KeyboardEvent` in your listeners.
     */
    function createEventHandler<K extends keyof WindowEventMap>(
      throttle: boolean,
      callbacks: readonly EventListener[]
    ): (event: WindowEventMap[K]) => void {
      let running = false;
      const runCallbacks = (event: WindowEventMap[K]) => () => {
        for (let i = 0; i < callbacks.length; i += 1) {
          callbacks[i](event);
        }

        running = false;
      };

      return function eventHandler(event) {
        if (!throttle) {
          runCallbacks(event)();
          return;
        }

        if (running) {
          return;
        }

        running = true;
        window.requestAnimationFrame(runCallbacks(event));
      };
    }

    /**
     * Creates a throttled event handler for the provided event type and event
     * target.
     */
    function createDelegatedEventHandler(
      eventType: string,
      eventTarget: DelegatedEventTarget = window,
      throttle = false,
      options?: boolean | AddEventListenerOptions
    ): DelegatedEventHandler {
      const callbacks: EventListener[] = [];
      const handler = createEventHandler(throttle, callbacks);

      return {
        /**
         * Attempts to add the provided callback to the list of callbacks for the
         * throttled event. If this is the first callback to be added, the throttled
         * event will also be started.
         */
        add: (callback: EventListener) => {
          if (!callbacks.length) {
            eventTarget.addEventListener(eventType, handler, options);
          }

          if (callbacks.indexOf(callback) === -1) {
            callbacks.push(callback);
          }
        },

        /**
         * Attempts to remove the provided callback from the list of callbacks for
         * the throttled event. If this is the last callback that was removed, the
         * throttled event will also be stopped.
         */
        remove: (callback: EventListener) => {
          const i = callbacks.indexOf(callback);
          if (i >= 0) {
            callbacks.splice(i, 1);

            if (!callbacks.length) {
              eventTarget.removeEventListener(eventType, handler, options);
            }
          }
        },
      };
    }

    /**
     * Creates a delegated event listener using custom events. Most of this code
     * comes from the MDN about resize listeners.
     *
     * This will return an object for adding or removing event handlers for the
     * provided `eventType` since only one base throttled event listener will be
     * created. Each callback that is added will be called with the event each time
     * the event is triggered. This does mean that you will manually need to remove
     * your callback like normal or else it can be called when no longer in use.
     * This also means that it doesn't "hurt" to call this function without
     * immediately calling the `add` function since the event won't start until
     * there is at least 1 callback.
     *
     * @see https://developer.mozilla.org/en-US/docs/Web/Events/resize#Examples
     * @param eventType - One of the event types that should be used to create a
     * delegated event for. This should be things like resize, click, scroll, etc.
     * @param eventTarget - The target that should have the delegated event handler
     * attached to. This is normally the window, but can be any element as needed.
     * @param throttle - Boolean if the event should be throttled or not. Normally
     * only event types like resize or scroll should be throttled for performance
     * boosts, but anything can be.
     * @returns The delegated event handler that allows you to add or remove
     * `EventListener`s to that event.
     * @internal
     */
    export function delegateEvent(
      eventType: string,
      eventTarget: DelegatedEventTarget = window,
      throttle: boolean = eventType === "resize" || eventType === "scroll",
      options?: boolean | AddEventListenerOptions
    ): DelegatedEventHandler {
      let index = delegatedEvents.findIndex(
        (event) =>
          event.type === eventType &&
          event.target === eventTarget &&
          event.options === options &&
          event.throttle === throttle
      );

      if (index === -1) {
        delegatedEvents.push({
          type: eventType,
          target: eventTarget,
          options,
          throttle,
          handler: createDelegatedEventHandler(
            eventType,
            eventTarget,
            throttle,
            options
          ),
        });

        index = delegatedEvents.length - 1;
      }

      return delegatedEvents[index].handler;
    }