blob: bc6a8197eca1bf462a4b5e13f47f4a69691dfabe [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* Handles the Switch Access menu panel.
* @implements {PanelInterface}
*/
class Panel {
constructor() {
/**
* The menu manager.
* @private {MenuManager}
*/
this.menuManager_;
/**
* Reference to switch access.
* @private {SwitchAccessInterface}
*/
this.switchAccess_;
/**
* Reference to the menu panel element.
* @private {Element}
*/
this.panel_;
/**
* ID of the current menu being shown in the menu panel.
* @private {?SAConstants.MenuId}
*/
this.currentMenuId_;
}
/**
* Initialize the panel and buttons.
*/
init() {
this.panel_ = document.getElementById(SAConstants.MENU_PANEL_ID);
let menuList = Object.values(SAConstants.MenuId);
for (const menuId of menuList) {
this.updateButtonOrder_(menuId);
const menu = document.getElementById(menuId);
for (const button of menu.children) {
this.setupButton_(button);
}
}
const background = chrome.extension.getBackgroundPage();
if (background.document.readyState === 'complete')
this.connectToBackground();
else
background.addEventListener('load', this.connectToBackground.bind(this));
this.addTranslatedMessagesToDom_();
}
/**
* Once both the menu panel and the background page have loaded, pass a
* reference to this object for communication.
*/
connectToBackground() {
this.switchAccess_ = chrome.extension.getBackgroundPage().switchAccess;
this.menuManager_ = this.switchAccess_.connectMenuPanel(this);
}
/**
* Adds an event listener to the given button to perform
* the corresponding menu action when clicked.
* @param {!Element} button
* @private
*/
setupButton_(button) {
let action = button.id;
button.addEventListener('click', function(action) {
this.menuManager_.performAction(action);
}.bind(this, action));
}
/**
* Get the HTML element for the back button.
* @return {Element}
*/
backButtonElement() {
return document.getElementById(SAConstants.BACK_ID);
}
/**
* Temporary function, until multiple focus rings is implemented.
* Puts a focus ring around the given menu item.
* TODO(crbug/925103): Implement multiple focus rings.
*
* @param {string} id
* @param {boolean} enable
*/
setFocusRing(id, enable) {
this.updateClass_(id, SAConstants.Focus.CLASS, enable);
return;
}
/**
* Sets the actions in the menu panel to the actions in |actions| from
* the menu with the given |menuId|.
* @param {!Array<string>} actions
* @param {!SAConstants.MenuId} menuId
* @public
*/
setActions(actions, menuId) {
const menu = document.getElementById(menuId);
const menuButtons = Array.from(menu.children);
// Add the menu to the panel if it is not already being shown.
if (menuId !== this.currentMenuId_) {
this.clear();
this.panel_.appendChild(menu);
menu.hidden = false;
}
// Hide menu actions not applicable to the current node.
for (const button of menuButtons) {
button.hidden = !actions.includes(button.id);
}
this.currentMenuId_ = menuId;
this.setHeight_(actions.length);
}
/**
* Clears the current menu from the panel.
* @public
*/
clear() {
if (this.currentMenuId_) {
const menu = document.getElementById(this.currentMenuId_);
document.body.appendChild(menu);
menu.hidden = true;
this.currentMenuId_ = null;
}
}
/**
* Get the id of the current menu being shown in the panel. A null
* id indicates that no menu is currently being shown in the panel.
* @return {?SAConstants.MenuId}
* @public
*/
currentMenuId() {
return this.currentMenuId_;
}
/**
* Update the position attributes of the buttons based on the order of IDs
* in the button order array parameter.
* TODO(b/994256) : Use this to set custom menu orders for users.
* @param {!Array<string>} buttonOrder
* @private
*/
updatePositionAttributes_(buttonOrder, menuId) {
this.menuManager_.exit();
for (let pos = 0; pos < buttonOrder.length; pos++) {
let buttonPosition = pos;
let button = document.getElementById(buttonOrder[pos]);
button.setAttribute('data-position', String(buttonPosition));
}
this.updateButtonOrder_(menuId);
}
/**
* Update the order of the buttons based on their position attributes.
* Lower numbers will be put into the menu first.
* @param {string} menuId
* @private
*/
updateButtonOrder_(menuId) {
let buttonList = document.getElementById(menuId);
let menuButtons = buttonList.children;
// Call slice() on menuButtons indirectly, as .children returns an
// HTMLCollection rather than an array.
let buttonArray = [].slice.call(menuButtons);
buttonArray.sort(this.buttonComesBefore_);
for (const button of buttonArray) {
if (button.id) {
buttonList.appendChild(button);
}
}
}
/**
* Compare buttonA's and buttonB's position attributes and return the
* difference.
* Used to sort the array of buttons in terms of position attribute.
* @param {!Element} buttonA
* @param {!Element} buttonB
*/
buttonComesBefore_(buttonA, buttonB) {
const buttonAPos = parseInt(buttonA.getAttribute('data-position'), 10);
const buttonBPos = parseInt(buttonB.getAttribute('data-position'), 10);
return buttonAPos - buttonBPos;
}
/**
* Either adds or removes the class |className| for the element with the given
* |id|.
* @param {string} id
* @param {string} className
* @param {boolean} shouldAdd
*/
updateClass_(id, className, shouldAdd) {
const htmlNode = document.getElementById(id);
if (shouldAdd)
htmlNode.classList.add(className);
else
htmlNode.classList.remove(className);
}
/**
* Sets the height of the menu (minus the body padding) based on the number of
* actions in the menu. This is necessary because floated elements do not
* contribute to their parent's height, and the elements are floated to avoid
* arbitrary space being added between buttons.
*
* @param {number} numActions
*/
setHeight_(numActions) {
// TODO(anastasi): This should be a preference that the user can change.
const maxCols = 3;
const numRows = Math.ceil(numActions / maxCols);
let rowHeight;
if (this.switchAccess_.improvedTextInputEnabled()) {
rowHeight = 85;
const actions = document.getElementsByClassName('action');
for (let action of actions) {
action.classList.add('improvedTextInputEnabled');
}
} else {
rowHeight = 60;
}
const height = rowHeight * numRows;
this.panel_.style.height = height + 'px';
}
/**
* Processes an HTML DOM, replacing text content with translated text messages
* on elements marked up for translation. Elements whose class attributes
* contain the 'i18n' class name are expected to also have an msgid attribute.
* The value of the msgid attributes are looked up as message IDs and the
* resulting text is used as the text content of the elements.
*
* TODO(crbug/706981): Combine with similar function in SelectToSpeakOptions.
* @private
*/
addTranslatedMessagesToDom_() {
const elements = document.querySelectorAll('.i18n');
for (const element of elements) {
const messageId = element.getAttribute('msgid');
if (!messageId)
throw new Error('Element has no msgid attribute: ' + element);
const translatedMessage =
chrome.i18n.getMessage('switch_access_' + messageId);
if (element.tagName == 'INPUT')
element.setAttribute('placeholder', translatedMessage);
else
element.textContent = translatedMessage;
element.classList.add('i18n-processed');
}
}
}
let switchAccessMenuPanel = new Panel();
if (document.readyState === 'complete')
switchAccessMenuPanel.init();
else
window.addEventListener(
'load', switchAccessMenuPanel.init.bind(switchAccessMenuPanel));