blob: f5f7ac8436892e1b2f8463d0b666a4016bf32a08 [file] [log] [blame]
// Copyright 2021 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../strings.m.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {skColorToRgba} from 'chrome://resources/js/color_utils.js';
import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {SkColor} from 'chrome://resources/mojo/skia/public/mojom/skcolor.mojom-webui.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './app.html.js';
const ReadAnythingElementBase = WebUiListenerMixin(PolymerElement);
////////////////////////////////////////////////////////////
// Called by ReadAnythingPageHandler via callback router. //
////////////////////////////////////////////////////////////
// The chrome.readAnything context is created by the ReadAnythingAppController
// which is only instantiated when the kReadAnything feature is enabled. This
// check if chrome.readAnything exists prevents runtime errors when the feature
// is disabled.
if (chrome.readAnything) {
chrome.readAnything.updateContent = () => {
const readAnythingApp = document.querySelector('read-anything-app');
assert(readAnythingApp);
readAnythingApp.updateContent();
};
chrome.readAnything.updateTheme = () => {
const readAnythingApp = document.querySelector('read-anything-app');
assert(readAnythingApp);
readAnythingApp.updateTheme();
};
}
export class ReadAnythingElement extends ReadAnythingElementBase {
static get is() {
return 'read-anything-app';
}
static get template() {
return getTemplate();
}
// Defines the valid font names that can be passed to front-end and maps
// them to a corresponding class style in app.html. Must stay in-sync with
// the names set in read_anything_font_model.cc.
// TODO(1266555): The displayed names of the fonts should be messages that
// will be translated to other languages.
private defaultFontName: string = 'Standard font';
private validFontNames: Array<{name: string, css: string}> = [
{name: 'Standard font', css: 'Standard font'},
{name: 'Sans-serif', css: 'sans-serif'},
{name: 'Serif', css: 'serif'},
{name: 'Avenir', css: 'avenir'},
{name: 'Comic Neue', css: '"Comic Neue"'},
{name: 'Comic Sans MS', css: '"Comic Sans MS"'},
{name: 'Poppins', css: 'poppins'},
];
override connectedCallback() {
super.connectedCallback();
if (chrome.readAnything) {
chrome.readAnything.onConnected();
}
}
private buildSubtree_(nodeId: number): Node {
let htmlTag = chrome.readAnything.getHtmlTag(nodeId);
// Text nodes do not have an html tag.
if (!htmlTag.length) {
const textContent = chrome.readAnything.getTextContent(nodeId);
return document.createTextNode(textContent);
}
// getHtmlTag might return '#document' which is not a valid to pass to
// createElement.
if (htmlTag === '#document') {
htmlTag = 'div';
}
const element = document.createElement(htmlTag);
const direction = chrome.readAnything.getTextDirection(nodeId);
if (direction) {
element.setAttribute('dir', direction);
}
const url = chrome.readAnything.getUrl(nodeId);
if (url && element.nodeName === 'A') {
element.setAttribute('href', url);
element.onclick = () => {
chrome.readAnything.onLinkClicked(nodeId);
};
}
const language = chrome.readAnything.getLanguage(nodeId);
if (language) {
element.setAttribute('lang', language);
}
this.appendChildSubtrees_(element, nodeId);
return element;
}
private appendChildSubtrees_(node: Node, nodeId: number) {
for (const childNodeId of chrome.readAnything.getChildren(nodeId)) {
const childNode = this.buildSubtree_(childNodeId);
node.appendChild(childNode);
}
}
updateContent() {
const shadowRoot = this.shadowRoot;
if (!shadowRoot) {
return;
}
const container = shadowRoot.getElementById('container');
if (!container) {
return;
}
// Remove all children from container. Use `replaceChildren` rather than
// setting `innerHTML = ''` in order to remove all listeners, too.
container.replaceChildren();
// Construct a dom subtree starting with the display root and append it to
// the container. The display root may be invalid if there are no content
// nodes and no selection.
// This does not use polymer's templating abstraction, which
// would create a shadow node element representing each AXNode, because
// experimentation found the shadow node creation to be ~8-10x slower than
// constructing and appending nodes directly to the container element.
const rootId = chrome.readAnything.rootId;
if (!rootId) {
return;
}
const node = this.buildSubtree_(rootId);
container.appendChild(node);
}
validatedFontName(): string {
// Validate that the given font name is a valid choice, or use the default.
const validFontName = this.validFontNames.find(
(f: {name: string}) => f.name === chrome.readAnything.fontName);
return validFontName ? validFontName.css : this.defaultFontName;
}
updateTheme() {
const foregroundColor:
SkColor = {value: chrome.readAnything.foregroundColor};
const backgroundColor:
SkColor = {value: chrome.readAnything.backgroundColor};
this.updateStyles({
'--background-color': skColorToRgba(backgroundColor),
'--font-family': this.validatedFontName(),
'--font-size': chrome.readAnything.fontSize + 'em',
'--foreground-color': skColorToRgba(foregroundColor),
'--letter-spacing': chrome.readAnything.letterSpacing + 'em',
'--line-height': chrome.readAnything.lineSpacing,
});
}
}
declare global {
interface HTMLElementTagNameMap {
'read-anything-app': ReadAnythingElement;
}
}
customElements.define(ReadAnythingElement.is, ReadAnythingElement);