blob: 0620e0098d7ddf2ec4bce7f4eec41208dae5c4c9 [file] [log] [blame]
// 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 {
constructor() {
this.initPage_();
}
static async init() {
if (LogPage.instance) {
throw new Error('LogPage can only be initiated once.');
}
LogPage.instance = new LogPage();
await LogPage.instance.update();
}
/**
* @param {!SerializableLog} log
* @private
*/
addLogToPage_(log) {
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);
toggleCheckbox.type = InputType.CHECKBOX;
toggleCheckbox.checked = true;
toggleCheckbox.onclick = event => textWrapper.hidden =
!event.target.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);
div.appendChild(p);
}
/**
* @param {!LogType} type
* @return {string}
* @private
*/
checkboxId_(type) {
return type + 'Filter';
}
/**
* @param {!LogType} type
* @param {boolean} checked
* @private
*/
createFilterCheckbox_(type, checked) {
const label = document.createElement(ElementName.LABEL);
const input = document.createElement(ElementName.INPUT);
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);
document.getElementById(IdName.FILTER).appendChild(label);
}
/** @private */
getDownloadFileName_() {
const date = new Date();
return [
'chromevox_logpage',
date.getMonth() + 1,
date.getDate(),
date.getHours(),
date.getMinutes(),
date.getSeconds(),
].join('_') +
'.txt';
}
/** @private */
initPage_() {
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);
}
const clearLogButton = document.getElementById(IdName.CLEAR);
clearLogButton.onclick = () => this.onClear_();
const saveLogButton = document.getElementById(IdName.SAVE);
saveLogButton.onclick = event => this.onSave_(event);
}
/**
* @param {!LogType} type
* @private
*/
isEnabled_(type) {
return document.getElementById(this.checkboxId_(type)).checked;
}
/**
* @param {Element} log
* @private
*/
logToString_(log) {
const logText = [];
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_() {
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.
* @param {Event} event
* @private
*/
onSave_(event) {
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);
a.download = this.getDownloadFileName_();
a.href = 'data:text/plain; charset=utf-8,' + encodeURI(outputText);
a.click();
}
/** Update the logs. */
async update() {
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_() {
const urlParams = [];
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.
* @param {!string} dateStr
* @return {!string}
* @private
*/
formatTimeStamp_(dateStr) {
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;
}
}
/** @type {LogPage} */
LogPage.instance;
// Local to module.
/** @enum {string} */
const ClassName = {
FILTER: 'log-filter',
TEXT: 'log-text',
TIME: 'log-time-tag',
TYPE: 'log-type-tag',
};
/** @enum {string} */
const ElementName = {
ANCHOR: 'a',
INPUT: 'input',
LABEL: 'label',
PARAGRAPH: 'p',
PRE: 'pre',
SPAN: 'span',
};
/** @enum {string} */
const EventType = {
CLICK: 'click',
};
/** @enum {string} */
const IdName = {
CLEAR: 'clearLog',
FILTER: 'logFilters',
LIST: 'logList',
SAVE: 'saveLog',
};
/** @enum {string} */
const InputType = {
CHECKBOX: 'checkbox',
};