// 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',
}