chromium/chrome/browser/resources/chromeos/accessibility/common/event_handler.ts

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

type AutomationEvent = chrome.automation.AutomationEvent;
type AutomationNode = chrome.automation.AutomationNode;
import EventType = chrome.automation.EventType;

interface EventHandlerOptions {
  /**
   * Whether to ignore events where the target is not the provided node.
   */
  exactMatch?: boolean;

  /**
   * True if the event should be processed before it has reached the target
   * node, false if it should be processed after.
   */
  capture?: boolean;

  /**
   * True if the event listeners should automatically be removed when the
   * callback is called once.
   */
  listenOnce?: boolean;

  /**
   * A predicate for what events will be processed.
   */
  predicate?: (event: AutomationEvent) => boolean;
}

/**
 * This class wraps AutomationNode event listeners, adding some convenience
 * functions.
 */
export class EventHandler {
  private nodes_: AutomationNode[];
  private types_: EventType[];
  private callback_: ((event: AutomationEvent) => void)|null;
  private capture_: boolean;
  private exactMatch_: boolean;
  private listenOnce_: boolean;
  private listening_ = false;
  private predicate_: (event: AutomationEvent) => boolean;
  private handler_: (event: AutomationEvent) => void;

  constructor(
      nodes: AutomationNode|AutomationNode[], types: EventType|EventType[],
      callback: (event: AutomationEvent) => void,
      options: EventHandlerOptions = {}) {
    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
    this.types_ = types instanceof Array ? types : [types];
    this.callback_ = callback;
    this.capture_ = options.capture || false;
    this.exactMatch_ = options.exactMatch || false;
    this.listenOnce_ = options.listenOnce || false;

    /**
     * Default is a function that always returns true.
     */
    this.predicate_ = options.predicate || (_e => true);

    this.handler_ = event => this.handleEvent_(event);
  }

  /** Starts listening to events. */
  start(): void {
    if (this.listening_) {
      return;
    }

    for (const node of this.nodes_) {
      for (const type of this.types_) {
        node.addEventListener(type, this.handler_, this.capture_);
      }
    }
    this.listening_ = true;
  }

  /** Stops listening or handling future events. */
  stop(): void {
    for (const node of this.nodes_) {
      for (const type of this.types_) {
        node.removeEventListener(type, this.handler_, this.capture_);
      }
    }
    this.listening_ = false;
  }

  /**
   * @return Whether this EventHandler is currently listening for events.
   */
  listening(): boolean {
    return this.listening_;
  }

  setCallback(callback: ((event: AutomationEvent) => void)|null): void {
    this.callback_ = callback;
  }

  /**
   * Changes what nodes are being listened to. Removes listeners from existing
   *     nodes before adding listeners on new nodes.
   */
  setNodes(nodes: AutomationNode|AutomationNode[]): void {
    const wasListening = this.listening_;
    // TODO(b/318557827): Shouldn't this be: if (wasListening) this.stop()?
    this.stop();
    this.nodes_ = nodes instanceof Array ? nodes : [nodes];
    if (wasListening) {
      this.start();
    }
  }

  /**
   * Adds another node to the set of nodes being listened to.
   */
  addNode(node: AutomationNode): void {
    this.nodes_.push(node);

    if (this.listening_) {
      for (const type of this.types_) {
        node.addEventListener(type, this.handler_, this.capture_);
      }
    }
  }

  /**
   * Removes a specific node from the set of nodes being listened to.
   */
  removeNode(node: AutomationNode): void {
    this.nodes_ = this.nodes_.filter(n => n !== node);

    if (this.listening_) {
      for (const type of this.types_) {
        node.removeEventListener(type, this.handler_, this.capture_);
      }
    }
  }

  private handleEvent_(event: AutomationEvent): void {
    if (this.exactMatch_ && !this.nodes_.includes(event.target)) {
      return;
    }

    if (!this.predicate_(event)) {
      return;
    }

    if (this.listenOnce_) {
      this.stop();
    }

    if (this.callback_) {
      this.callback_(event);
    }
  }
}