All files / utils gesture-wheel.ts

100% Statements 50/50
96.55% Branches 28/29
100% Functions 1/1
100% Lines 49/49

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        94x 94x       94x   94x 94x   94x                                                                                       94x           318x 41x     318x 318x 318x 318x 318x   318x 351x 5x     346x 346x 346x 346x 346x 346x   346x   79x     79x 63x     79x   79x 79x 267x     7x 7x     346x 346x 346x   346x   346x 315x 31x   2x       318x 3x 315x 2x 313x 73x   240x           315x                      
/**
 * @module Utils
 */
 
import * as MC from "@lisn/globals/minification-constants";
import * as MH from "@lisn/globals/minification-helpers";
 
import { Direction, GestureIntent } from "@lisn/globals/types";
 
import { getVectorDirection } from "@lisn/utils/directions";
 
import { havingMaxAbs } from "@lisn/utils/math";
import { normalizeWheel } from "@lisn/utils/normalize-wheel";
 
import { GestureFragment, addDeltaZ } from "@lisn/utils/gesture";
 
/**
 * Returns a {@link GestureFragment} for the given events. Only "wheel" events
 * will be considered.
 *
 * If there are no "wheel" events in the given list of events, returns `false`.
 *
 * The deltas of all events are summed together before determining final delta
 * and direction.
 *
 * If the events are of conflicting types, i.e. some scroll, some zoom, then
 * the intent will be "unknown" and the direction will be "ambiguous".
 *
 * If the deltas sum up to 0, the direction will be "none".
 *
 * **IMPORTANT NOTES ON THE DELTA VALUES**
 *
 * For wheel gestures the deltas are _highly_ unreliable, especially when
 * zooming (Control + wheel or pinching trackpad). You should not assume they
 * correspond to the would-be scroll or zoom amount that the browser would do.
 * But they can be used to determine relative amounts for animating, etc.
 *
 * If the browser reports the delta values of a WheelEvent to be in mode "line",
 * then a configurable fixed value is used
 * ({@link Settings.settings.deltaLineHeight | settings.deltaLineHeight}).
 *
 * If the browser reports the delta values of a WheelEvent to be in mode "page",
 * then a configurable fixed value is used
 * ({@link Settings.settings.deltaPageWidth | settings.deltaPageWidth} and
 * ({@link Settings.settings.deltaPageHeight | settings.deltaPageHeight}).
 *
 * For zoom intents `deltaZ` is based on what the browser reports as the
 * `deltaY`, which in most browsers roughly corresponds to a percentage zoom
 * factor.
 *
 * @param [options.angleDiffThreshold] See {@link getVectorDirection}.
 *                                     Default is 5.
 *
 * @returns `false` if there are no "wheel" events in the list, otherwise a
 * {@link GestureFragment}.
 *
 * @category Gestures
 */
export const getWheelGestureFragment = (
  events: Event | readonly Event[],
  options?: {
    angleDiffThreshold?: number;
  },
): GestureFragment | null | false => {
  if (!MH.isIterableObject(events)) {
    events = [events];
  }
 
  let direction: Direction = MC.S_NONE;
  let intent: GestureIntent | null = null;
  let deltaX = 0,
    deltaY = 0,
    deltaZ = 1;
 
  for (const event of events) {
    if (!MH.isWheelEvent(event) || event.type !== MC.S_WHEEL) {
      continue;
    }
 
    const data = normalizeWheel(event);
    let thisIntent: GestureIntent = MC.S_SCROLL;
    let thisDeltaX = data.pixelX;
    let thisDeltaY = data.pixelY;
    let thisDeltaZ = 1;
    const maxDelta = havingMaxAbs(thisDeltaX, thisDeltaY);
 
    if (event.ctrlKey && !thisDeltaX) {
      // Browsers report negative deltaY for zoom in, so swap sign
      let percentage = -maxDelta;
      // If it's more than 50, assume it's a mouse wheel => delta is roughly
      // multiple of 10%. Otherwise a trackpad => delta is roughly multiple of 1%
      if (MH.abs(percentage) >= 50) {
        percentage /= 10;
      }
 
      thisDeltaZ = 1 + percentage / 100;
 
      thisDeltaX = thisDeltaY = 0;
      thisIntent = MC.S_ZOOM;
    } else if (event.shiftKey && !thisDeltaX) {
      // Holding Shift while turning wheel or swiping trackpad in vertically
      // results in sideways scroll.
      thisDeltaX = thisDeltaY;
      thisDeltaY = 0;
    }
 
    deltaX += thisDeltaX;
    deltaY += thisDeltaY;
    deltaZ = addDeltaZ(deltaZ, thisDeltaZ);
 
    Iif (!thisIntent) {
      // not a relevant key
    } else if (!intent) {
      intent = thisIntent;
    } else if (intent !== thisIntent) {
      // mixture of zoom and scroll
      intent = MC.S_UNKNOWN;
    }
  }
 
  if (!intent) {
    return false; // no relevant events
  } else if (intent === MC.S_UNKNOWN) {
    direction = MC.S_AMBIGUOUS;
  } else if (intent === MC.S_ZOOM) {
    direction = deltaZ > 1 ? MC.S_IN : deltaZ < 1 ? MC.S_OUT : MC.S_NONE;
  } else {
    direction = getVectorDirection(
      [deltaX, deltaY],
      options?.angleDiffThreshold,
    );
  }
 
  return direction === MC.S_NONE
    ? false
    : {
        device: MC.S_WHEEL,
        direction,
        intent,
        deltaX,
        deltaY,
        deltaZ,
      };
};