chromium/chrome/browser/resources/chromeos/accessibility/common/repeated_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 AutomationNode = chrome.automation.AutomationNode;
type AutomationEvent = chrome.automation.AutomationEvent;
import EventType = chrome.automation.EventType;

interface RepeatedEventHandlerOptions {
  /**
   * True if events should only be handled if the provided node is the target.
   */
  exactMatch?: boolean;

  /**
   * True if events for children of |node| should be handled before they reach
   * the target node; false to be handled after the target node.
   */
  capture?: boolean;

  /**
   * True if a listener should be added to all ancestors of the provided nodes.
   */
  allAncestors?: boolean;
}

/**
 * This class assists with processing repeated events in nontrivial ways by
 * allowing only the most recent event to be processed.
 */
export class RepeatedEventHandler {
  private eventStack_: AutomationEvent[] = [];
  private nodes_: AutomationNode[];
  private type_: EventType;
  private callback_: (event: AutomationEvent) => void;
  private exactMatch_: boolean;
  private capture_: boolean;
  private listening_ = false;
  private handler_: (event: AutomationEvent) => void;

  constructor(
      nodes: AutomationNode|AutomationNode[], type: EventType,
      callback: (event: AutomationEvent) => void,
      options: RepeatedEventHandlerOptions = {}) {
    this.nodes_ = nodes instanceof Array ? nodes : [nodes];

    if (options.allAncestors) {
      nodes = this.nodes_;  // Make sure nodes is an array.
      this.nodes_ = [];
      for (let node of nodes) {
        while (node) {
          this.nodes_.push(node);
          // TODO(b/314203187): Not null asserted, check these to make sure they
          // are correct.
          node = node.parent!;
        }
      }
    }

    this.type_ = type;
    this.callback_ = callback;
    this.exactMatch_ = options.exactMatch || false;
    this.capture_ = options.capture || false;
    this.handler_ = event => this.onEvent_(event);

    this.start();
  }

  /** Starts listening or handling events. */
  start(): void {
    if (this.listening_) {
      return;
    }
    this.listening_ = true;
    for (const node of this.nodes_) {
      node.addEventListener(this.type_, this.handler_, this.capture_);
    }
  }

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

  private onEvent_(event: AutomationEvent): void {
    this.eventStack_.push(event);
    setTimeout(() => this.handleEvent_(), 0);
  }

  private handleEvent_(): void {
    if (!this.listening_ || this.eventStack_.length === 0) {
      return;
    }

    // TODO(b/314203187): Not null asserted, check these to make sure they are
    // correct.
    const event = this.eventStack_.pop()!;
    if (this.exactMatch_ && !this.nodes_.includes(event!.target)) {
      return;
    }
    this.eventStack_ = [];

    this.callback_(event);
  }
}