chromium/chrome/browser/resources/chromeos/accessibility/chromevox/log_page/log.ts

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

/**
 * @fileoverview ChromeVox log page.
 */
import {BackgroundBridge} from '../common/background_bridge.js';
import {LogType, SerializableLog} from '../common/log_types.js';

/** Class to manage the log page. */
export class LogPage {
  static instance: LogPage;

  constructor() {
    this.initPage_();
  }

  static async init(): Promise<void> {
    if (LogPage.instance) {
      throw new Error('LogPage can only be initiated once.');
    }
    LogPage.instance = new LogPage();
    await LogPage.instance.update();
  }

  private addLogToPage_(log: SerializableLog): void {
    const div = document.getElementById(IdName.LIST);
    const p = document.createElement(ElementName.PARAGRAPH);

    const typeName = document.createElement(ElementName.SPAN);
    typeName.textContent = log.logType;
    typeName.className = ClassName.TYPE;
    p.appendChild(typeName);

    const timeStamp = document.createElement(ElementName.SPAN);
    timeStamp.textContent = this.formatTimeStamp_(log.date);
    timeStamp.className = ClassName.TIME;
    p.appendChild(timeStamp);

    /** Add hide tree button when logType is tree. */
    if (log.logType === LogType.TREE) {
      const toggle = document.createElement(ElementName.LABEL);
      const toggleCheckbox =
          document.createElement(ElementName.INPUT) as HTMLInputElement;
      toggleCheckbox.type = InputType.CHECKBOX;
      toggleCheckbox.checked = true;
      toggleCheckbox.onclick = event => textWrapper.hidden =
          !(event.target as HTMLInputElement).checked;

      const toggleText = document.createElement(ElementName.SPAN);
      toggleText.textContent = 'show tree';
      toggle.appendChild(toggleCheckbox);
      toggle.appendChild(toggleText);
      p.appendChild(toggle);
    }

    /** textWrapper should be in block scope, not function scope. */
    const textWrapper = document.createElement(ElementName.PRE);
    textWrapper.textContent = log.value;
    textWrapper.className = ClassName.TEXT;
    p.appendChild(textWrapper);

    // TODO(b/314203187): Not null asserted, check that this is correct.
    div!.appendChild(p);
  }

  private checkboxId_(type: LogType): string {
    return type + 'Filter';
  }

  private createFilterCheckbox_(type: LogType, checked: boolean): void {
    const label = document.createElement(ElementName.LABEL) as HTMLLabelElement;
    const input = document.createElement(ElementName.INPUT) as HTMLInputElement;
    input.id = this.checkboxId_(type);
    input.type = InputType.CHECKBOX;
    input.classList.add(ClassName.FILTER);
    input.checked = checked;
    input.addEventListener(EventType.CLICK, () => this.updateUrlParams_());
    label.appendChild(input);

    const span = document.createElement(ElementName.SPAN);
    span.textContent = type;
    label.appendChild(span);

    // TODO(b/314203187): Not null asserted, check that this is correct.
    document.getElementById(IdName.FILTER)!.appendChild(label);
  }

  private getDownloadFileName_(): string {
    const date = new Date();
    return [
      'chromevox_logpage',
      date.getMonth() + 1,
      date.getDate(),
      date.getHours(),
      date.getMinutes(),
      date.getSeconds(),
    ].join('_') +
        '.txt';
  }

  private initPage_(): void {
    const params = new URLSearchParams(location.search);
    for (const type of Object.values(LogType)) {
      const enabled =
          (params.get(type) === String(true) || params.get(type) === null);
      this.createFilterCheckbox_(type, enabled);
    }

    // TODO(b/314203187): Not null asserted, check that this is correct.
    const clearLogButton = document.getElementById(IdName.CLEAR);
    clearLogButton!.onclick = () => this.onClear_();

    const saveLogButton = document.getElementById(IdName.SAVE);
    saveLogButton!.onclick = event => this.onSave_(event);
  }

  private isEnabled_(type: LogType): boolean {
    const element =
        document.getElementById(this.checkboxId_(type)) as HTMLInputElement;
    return element.checked;
  }

  private logToString_(log: Element): string {
    const logText: string[] = [];
    // TODO(b/314203187): Not null asserted, check that this is correct.
    logText.push(log.querySelector(`.${ClassName.TYPE}`)!.textContent!);
    logText.push(log.querySelector(`.${ClassName.TIME}`)!.textContent!);
    logText.push(log.querySelector(`.${ClassName.TEXT}`)!.textContent!);
    return logText.join(' ');
  }

  private async onClear_(): Promise<void> {
    await BackgroundBridge.LogStore.clearLog();
    location.reload();
  }

  /**
   * When saveLog button is clicked this function runs.
   * Save the current log appeared in the page as a plain text.
   */
  private onSave_(_event: Event): void {
    let outputText = '';
    const logs =
        document.querySelectorAll(`#${IdName.LIST} ${ElementName.PARAGRAPH}`);
    for (const log of logs) {
      outputText += this.logToString_(log) + '\n';
    }

    const a = document.createElement(ElementName.ANCHOR) as HTMLAnchorElement;
    a.download = this.getDownloadFileName_();
    a.href = 'data:text/plain; charset=utf-8,' + encodeURI(outputText);
    a.click();
  }

  /** Update the logs. */
  async update(): Promise<void> {
    const logs = await BackgroundBridge.LogStore.getLogs();
    if (!logs) {
      return;
    }

    for (const log of logs) {
      if (this.isEnabled_(log.logType)) {
        this.addLogToPage_(log);
      }
    }
  }

  /** Update the URL parameter based on the checkboxes. */
  private updateUrlParams_(): void {
    const urlParams: string[] = [];
    for (const type of Object.values(LogType)) {
      urlParams.push(type + 'Filter=' + LogPage.instance.isEnabled_(type));
    }
    location.search = '?' + urlParams.join('&');
  }

  /**
   * Format time stamp.
   * In this log, events are dispatched many times in a short time, so
   * milliseconds order time stamp is required.
   */
  private formatTimeStamp_(dateStr: string): string {
    const date = new Date(dateStr);
    let time = date.getTime();
    time -= date.getTimezoneOffset() * 1000 * 60;
    let timeStr =
        ('00' + Math.floor(time / 1000 / 60 / 60) % 24).slice(-2) + ':';
    timeStr += ('00' + Math.floor(time / 1000 / 60) % 60).slice(-2) + ':';
    timeStr += ('00' + Math.floor(time / 1000) % 60).slice(-2) + '.';
    timeStr += ('000' + time % 1000).slice(-3);
    return timeStr;
  }
}

// Local to module.

enum ClassName {
  FILTER = 'log-filter',
  TEXT = 'log-text',
  TIME = 'log-time-tag',
  TYPE = 'log-type-tag',
}

enum ElementName {
  ANCHOR = 'a',
  INPUT = 'input',
  LABEL = 'label',
  PARAGRAPH = 'p',
  PRE = 'pre',
  SPAN = 'span',
}

enum EventType {
  CLICK = 'click',
}

enum IdName {
  CLEAR = 'clearLog',
  FILTER = 'logFilters',
  LIST = 'logList',
  SAVE = 'saveLog',
}

enum InputType {
  CHECKBOX = 'checkbox',
}