chromium/chrome/browser/resources/pdf/swipe_detector.ts

// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {assert} from 'chrome://resources/js/assert.js';

/**
 * The longest period of time in milliseconds for a horizontal touch movement to
 * be considered as a swipe.
 */
const SWIPE_TIMER_INTERVAL_MS: number = 200;

/* The minimum travel distance on the x axis for a swipe. */
const SWIPE_X_DIST_MIN: number = 150;

/* The maximum travel distance on the y axis for a swipe. */
const SWIPE_Y_DIST_MAX: number = 100;

export interface SwipeEvent {
  type: string;
  detail: SwipeDirection;
}

/** Enumeration of swipe directions. */
export enum SwipeDirection {
  RIGHT_TO_LEFT = 0,
  LEFT_TO_RIGHT = 1,
}

// A class that listens for touch events and produces events when these
// touches form swipe gestures.
export class SwipeDetector {
  private element_: HTMLElement;
  private isPresentationMode_: boolean = false;
  private swipeStartEvent_: TouchEvent|null = null;

  private elapsedTimeForTesting_: number|null = null;

  private eventTarget_: EventTarget = new EventTarget();

  /** @param element The element to monitor for touch gestures. */
  constructor(element: HTMLElement) {
    this.element_ = element;

    this.element_.addEventListener(
        'touchstart', (this.onTouchStart_.bind(this) as (p1: Event) => any),
        {passive: true});

    this.element_.addEventListener(
        'touchend', (this.onTouchEnd_.bind(this) as (p1: Event) => any),
        {passive: true});
    this.element_.addEventListener(
        'touchcancel', () => this.onTouchCancel_(), {passive: true});
  }

  /**
   * Public for tests. Allow manually setting the elapsed time for a swipe
   * action.
   */
  setElapsedTimerForTesting(time: number) {
    this.elapsedTimeForTesting_ = time;
  }

  setPresentationMode(enabled: boolean) {
    this.isPresentationMode_ = enabled;
  }

  getPresentationModeForTesting() {
    return this.isPresentationMode_;
  }

  getEventTarget(): EventTarget {
    return this.eventTarget_;
  }

  /**
   * Call the relevant listeners with the given swipe |direction|.
   * @param direction The direction of swipe action.
   */
  private notify_(direction: SwipeDirection) {
    this.eventTarget_.dispatchEvent(
        new CustomEvent('swipe', {detail: direction}));
  }

  /** The callback for touchstart events on the element. */
  private onTouchStart_(event: TouchEvent) {
    if (!this.isPresentationMode_) {
      return;
    }

    // If more than 1 finger touch the screen or there is already an ongoing
    // swipe detection process, there is no valid swipe event to keep track.
    if (event.touches.length !== 1 || this.swipeStartEvent_) {
      this.swipeStartEvent_ = null;
      return;
    }

    this.swipeStartEvent_ = event;
    return;
  }

  /** The callback for touchcancel events on the element. */
  private onTouchCancel_() {
    if (!this.isPresentationMode_ || !this.swipeStartEvent_) {
      return;
    }

    this.swipeStartEvent_ = null;
  }

  /** The callback for touchend events on the element. */
  private onTouchEnd_(event: TouchEvent) {
    if (!this.isPresentationMode_ || !this.swipeStartEvent_) {
      return;
    }

    if (event.touches.length !== 0 ||
        this.swipeStartEvent_.touches.length !== 1) {
      return;
    }

    const elapsedTime = this.elapsedTimeForTesting_ ?
        this.elapsedTimeForTesting_ :
        event.timeStamp - this.swipeStartEvent_.timeStamp;
    const swipeStartObj = this.swipeStartEvent_.changedTouches[0];
    assert(swipeStartObj);
    const swipeEndObj = event.changedTouches[0];
    assert(swipeEndObj);
    const distX = swipeEndObj.pageX - swipeStartObj.pageX;
    const distY = swipeEndObj.pageY - swipeStartObj.pageY;

    // If this is a valid swipe, notify its direction to the viewer.
    if (elapsedTime <= SWIPE_TIMER_INTERVAL_MS &&
        Math.abs(distX) >= SWIPE_X_DIST_MIN &&
        Math.abs(distY) <= SWIPE_Y_DIST_MAX) {
      const direction = distX > 0 ? SwipeDirection.LEFT_TO_RIGHT :
                                    SwipeDirection.RIGHT_TO_LEFT;
      this.notify_(direction);
    }

    this.swipeStartEvent_ = null;
  }
}