| // Copyright (c) 2013 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. |
| |
| import 'chrome://resources/js/action_link.js'; |
| |
| import {addWebUIListener} from 'chrome://resources/js/cr.m.js'; |
| import {$} from 'chrome://resources/js/util.m.js'; |
| |
| class BrowserProxy { |
| toggleAccessibility(processId, routingId, modeId, shouldRequestTree) { |
| chrome.send('toggleAccessibility', [{ |
| processId, |
| routingId, |
| modeId, |
| shouldRequestTree, |
| }]); |
| } |
| |
| requestNativeUITree(sessionId, requestType, allow, allowEmpty, deny) { |
| chrome.send('requestNativeUITree', [{ |
| sessionId, |
| requestType, |
| filters: {allow, allowEmpty, deny}, |
| }]); |
| } |
| |
| requestWebContentsTree( |
| processId, routingId, requestType, allow, allowEmpty, deny) { |
| chrome.send('requestWebContentsTree', [ |
| {processId, routingId, requestType, filters: {allow, allowEmpty, deny}} |
| ]); |
| } |
| |
| requestWidgetsTree(widgetId, requestType, allow, allowEmpty, deny) { |
| chrome.send( |
| 'requestWidgetsTree', |
| [{widgetId, requestType, filters: {allow, allowEmpty, deny}}]); |
| } |
| |
| requestAccessibilityEvents(processId, routingId, start) { |
| chrome.send('requestAccessibilityEvents', [{processId, routingId, start}]); |
| } |
| |
| setGlobalFlag(flagName, enabled) { |
| chrome.send('setGlobalFlag', [{flagName, enabled}]); |
| } |
| } |
| |
| const browserProxy = new BrowserProxy(); |
| |
| // Note: keep these values in sync with the values in |
| // ui/accessibility/ax_mode.h |
| const AXMode = { |
| kNativeAPIs: 1 << 0, |
| kWebContents: 1 << 1, |
| kInlineTextBoxes: 1 << 2, |
| kScreenReader: 1 << 3, |
| kHTML: 1 << 4, |
| kHTMLMetadata: 1 << 5, |
| kLabelImages: 1 << 6, |
| kPDF: 1 << 7, |
| |
| get kAXModeBasic() { |
| return AXMode.kNativeAPIs | AXMode.kWebContents; |
| }, |
| |
| get kAXModeWebContentsOnly() { |
| return AXMode.kWebContents | AXMode.kInlineTextBoxes | |
| AXMode.kScreenReader | AXMode.kHTML; |
| }, |
| |
| get kAXModeComplete() { |
| return AXMode.kNativeAPIs | AXMode.kWebContents | AXMode.kInlineTextBoxes | |
| AXMode.kScreenReader | AXMode.kHTML; |
| }, |
| |
| get kAXModeCompleteNoHTML() { |
| return AXMode.kNativeAPIs | AXMode.kWebContents | AXMode.kInlineTextBoxes | |
| AXMode.kScreenReader; |
| } |
| }; |
| |
| function requestData() { |
| const xhr = new XMLHttpRequest(); |
| xhr.open('GET', 'targets-data.json', false); |
| xhr.send(null); |
| if (xhr.status === 200) { |
| console.log(xhr.responseText); |
| return JSON.parse(xhr.responseText); |
| } |
| return []; |
| } |
| |
| function getIdFromData(data) { |
| if (data.type == 'page') { |
| return data.processId + '.' + data.routingId; |
| } else if (data.type == 'browser') { |
| return 'browser.' + data.sessionId; |
| } else if (data.type == 'widget') { |
| return 'widget.' + data.widgetId; |
| } else { |
| console.error('Unknown data type.', data); |
| return ''; |
| } |
| } |
| |
| function toggleAccessibility(data, element, mode, globalStateName) { |
| if (!data[globalStateName]) { |
| return; |
| } |
| |
| const id = getIdFromData(data); |
| const tree = $(id + ':tree'); |
| // If the tree is visible, request a new tree with the updated mode. |
| const shouldRequestTree = !!tree && tree.style.display != 'none'; |
| browserProxy.toggleAccessibility( |
| data.processId, data.routingId, mode, shouldRequestTree); |
| } |
| |
| function requestTree(data, element) { |
| const allow = $('filter-allow').value; |
| const allowEmpty = $('filter-allow-empty').value; |
| const deny = $('filter-deny').value; |
| window.localStorage['chrome-accessibility-filter-allow'] = allow; |
| window.localStorage['chrome-accessibility-filter-allow-empty'] = allowEmpty; |
| window.localStorage['chrome-accessibility-filter-deny'] = deny; |
| |
| // The calling |element| is a button with an id of the format |
| // <treeId>:<requestType>, where requestType is one of 'showOrRefreshTree', |
| // 'copyTree'. Send the request type to C++ so is calls the corresponding |
| // function with the result. |
| const requestType = element.id.split(':')[1]; |
| if (data.type == 'browser') { |
| const delay = $('native-ui-delay').value; |
| setTimeout(() => { |
| browserProxy.requestNativeUITree( |
| data.sessionId, requestType, allow, allowEmpty, deny); |
| }, delay); |
| } else if (data.type == 'widget') { |
| browserProxy.requestWidgetsTree( |
| data.widgetId, requestType, allow, allowEmpty, deny); |
| } else { |
| browserProxy.requestWebContentsTree( |
| data.processId, data.routingId, requestType, allow, allowEmpty, deny); |
| } |
| } |
| |
| function requestEvents(data, element) { |
| const start = element.textContent == 'Start recording'; |
| if (start) { |
| element.textContent = 'Stop recording'; |
| element.setAttribute('aria-expanded', 'true'); |
| |
| // Disable all other start recording buttons. UI reflects the fact that |
| // there can only be one accessibility recorder at once. |
| const buttons = document.getElementsByClassName('recordEventsButton'); |
| for (const button of buttons) { |
| if (button != element) { |
| button.disabled = true; |
| } |
| } |
| } else { |
| element.textContent = 'Start recording'; |
| element.setAttribute('aria-expanded', 'false'); |
| |
| // Enable all start recording buttons. |
| const buttons = document.getElementsByClassName('recordEventsButton'); |
| for (const button of buttons) { |
| if (button != element) { |
| button.disabled = false; |
| } |
| } |
| } |
| browserProxy.requestAccessibilityEvents( |
| data.processId, data.routingId, start); |
| } |
| |
| function initialize() { |
| console.log('initialize'); |
| const data = requestData(); |
| |
| bindCheckbox('native', data['native']); |
| bindCheckbox('web', data['web']); |
| bindCheckbox('text', data['text']); |
| bindCheckbox('screenreader', data['screenreader']); |
| bindCheckbox('html', data['html']); |
| bindCheckbox('label_images', data['labelImages']); |
| bindCheckbox('internal', data['internal']); |
| |
| $('pages').textContent = ''; |
| |
| const pages = data['pages']; |
| for (let i = 0; i < pages.length; i++) { |
| addToPagesList(pages[i]); |
| } |
| |
| const browsers = data['browsers']; |
| for (let i = 0; i < browsers.length; i++) { |
| addToBrowsersList(browsers[i]); |
| } |
| |
| if (data['viewsAccessibility']) { |
| const widgets = data['widgets']; |
| |
| if (widgets.length === 0) { |
| // There should always be at least 1 Widget displayed (for the current |
| // window). If this is not the case, and Views Accessibility is enabled, |
| // the only possibility is that Views Accessibility is not enabled for |
| // the current platform. Display a message to the user to indicate this. |
| $('widgets-not-supported').style.display = 'block'; |
| } else { |
| for (let i = 0; i < widgets.length; i++) { |
| addToWidgetsList(widgets[i]); |
| } |
| } |
| } else { |
| $('widgets').style.display = 'none'; |
| $('widgets-header').style.display = 'none'; |
| } |
| |
| // Cache filters so they're easily accessible on page refresh. |
| const allow = window.localStorage['chrome-accessibility-filter-allow']; |
| const allowEmpty = |
| window.localStorage['chrome-accessibility-filter-allow-empty']; |
| const deny = window.localStorage['chrome-accessibility-filter-deny']; |
| $('filter-allow').value = allow ? allow : '*'; |
| $('filter-allow-empty').value = allowEmpty ? allowEmpty : ''; |
| $('filter-deny').value = deny ? deny : ''; |
| |
| addWebUIListener('copyTree', copyTree); |
| addWebUIListener('showOrRefreshTree', showOrRefreshTree); |
| addWebUIListener('startOrStopEvents', startOrStopEvents); |
| } |
| |
| function bindCheckbox(name, value) { |
| if (value == 'on') { |
| $(name).checked = true; |
| } |
| if (value == 'disabled') { |
| $(name).disabled = true; |
| $(name).labels[0].classList.add('disabled'); |
| } |
| $(name).addEventListener('change', function() { |
| browserProxy.setGlobalFlag(name, $(name).checked); |
| document.location.reload(); |
| }); |
| } |
| |
| function addToPagesList(data) { |
| // TODO: iterate through data and pages rows instead |
| const id = getIdFromData(data); |
| const row = document.createElement('div'); |
| row.className = 'row'; |
| row.id = id; |
| formatRow(row, data, null); |
| |
| row.processId = data.processId; |
| row.routingId = data.routingId; |
| |
| const pages = $('pages'); |
| pages.appendChild(row); |
| } |
| |
| function addToBrowsersList(data) { |
| const id = getIdFromData(data); |
| const row = document.createElement('div'); |
| row.className = 'row'; |
| row.id = id; |
| formatRow(row, data, null); |
| |
| const browsers = $('browsers'); |
| browsers.appendChild(row); |
| } |
| |
| function addToWidgetsList(data) { |
| const id = getIdFromData(data); |
| const row = document.createElement('div'); |
| row.className = 'row'; |
| row.id = id; |
| formatRow(row, data, null); |
| |
| const widgets = $('widgets'); |
| widgets.appendChild(row); |
| } |
| |
| function formatRow(row, data, requestType) { |
| if (!('url' in data)) { |
| if ('error' in data) { |
| row.appendChild(createErrorMessageElement(data)); |
| return; |
| } |
| } |
| |
| if (data.type == 'page') { |
| const siteInfo = document.createElement('div'); |
| const properties = ['faviconUrl', 'name', 'url']; |
| for (let j = 0; j < properties.length; j++) { |
| siteInfo.appendChild(formatValue(data, properties[j])); |
| } |
| row.appendChild(siteInfo); |
| |
| row.appendChild(createModeElement(AXMode.kNativeAPIs, data, 'native')); |
| row.appendChild(createModeElement(AXMode.kWebContents, data, 'native')); |
| row.appendChild(createModeElement(AXMode.kInlineTextBoxes, data, 'web')); |
| row.appendChild(createModeElement(AXMode.kScreenReader, data, 'web')); |
| row.appendChild(createModeElement(AXMode.kHTML, data, 'web')); |
| row.appendChild(createModeElement(AXMode.kHTMLMetadata, data, 'metadata')); |
| row.appendChild( |
| createModeElement(AXMode.kLabelImages, data, 'labelImages')); |
| row.appendChild(createModeElement(AXMode.kPDF, data, 'pdf')); |
| } else { |
| const siteInfo = document.createElement('span'); |
| siteInfo.appendChild(formatValue(data, 'name')); |
| row.appendChild(siteInfo); |
| } |
| |
| row.appendChild(document.createTextNode(' | ')); |
| |
| const hasTree = 'tree' in data; |
| row.appendChild( |
| createShowAccessibilityTreeElement(data, row.id, requestType, hasTree)); |
| if (navigator.clipboard) { |
| row.appendChild(createCopyAccessibilityTreeElement(data, row.id)); |
| } |
| if (hasTree) { |
| row.appendChild(createHideAccessibilityTreeElement(row.id)); |
| } |
| // The accessibility event recorder currently only works for pages. |
| // TODO(abigailbklein): Add event recording for native as well. |
| if (data.type == 'page') { |
| row.appendChild( |
| createStartStopAccessibilityEventRecordingElement(data, row.id)); |
| } |
| |
| if (hasTree) { |
| row.appendChild(createAccessibilityOutputElement(data, row.id, 'tree')); |
| } else if ('eventLogs' in data) { |
| row.appendChild( |
| createAccessibilityOutputElement(data, row.id, 'eventLogs')); |
| } else if ('error' in data) { |
| row.appendChild(createErrorMessageElement(data)); |
| } |
| } |
| |
| function insertHeadingInline(parentElement, headingText, id) { |
| const h3 = document.createElement('h3'); |
| h3.textContent = headingText; |
| h3.style.display = 'inline'; |
| h3.id = id + ':title'; |
| parentElement.appendChild(h3); |
| } |
| |
| function formatValue(data, property) { |
| const value = data[property]; |
| |
| if (property == 'faviconUrl') { |
| const faviconElement = document.createElement('img'); |
| if (value) { |
| faviconElement.src = value; |
| } |
| faviconElement.alt = ''; |
| return faviconElement; |
| } |
| |
| let text = value ? String(value) : ''; |
| if (text.length > 100) { |
| text = text.substring(0, 100) + '\u2026'; |
| } // ellipsis |
| |
| const span = document.createElement('span'); |
| const content = ' ' + text + ' '; |
| if (property == 'name') { |
| const id = getIdFromData(data); |
| insertHeadingInline(span, content, id); |
| } else { |
| span.textContent = content; |
| } |
| span.className = property; |
| return span; |
| } |
| |
| function getNameForAccessibilityMode(mode) { |
| switch (mode) { |
| case AXMode.kNativeAPIs: |
| return 'Native'; |
| case AXMode.kWebContents: |
| return 'Web'; |
| case AXMode.kInlineTextBoxes: |
| return 'Inline text'; |
| case AXMode.kScreenReader: |
| return 'Screen reader'; |
| case AXMode.kHTML: |
| return 'HTML'; |
| case AXMode.kHTMLMetadata: |
| return 'HTML Metadata'; |
| case AXMode.kLabelImages: |
| return 'Label images'; |
| case AXMode.kPDF: |
| return 'PDF'; |
| } |
| return 'unknown'; |
| } |
| |
| function createModeElement(mode, data, globalStateName) { |
| const currentMode = data['a11yMode']; |
| const link = document.createElement('a', {is: 'action-link'}); |
| link.setAttribute('is', 'action-link'); |
| link.setAttribute('role', 'button'); |
| |
| const stateText = ((currentMode & mode) != 0) ? 'true' : 'false'; |
| const isEnabled = data[globalStateName]; |
| if (isEnabled) { |
| link.textContent = getNameForAccessibilityMode(mode) + ': ' + stateText; |
| } else { |
| link.textContent = getNameForAccessibilityMode(mode) + ': disabled'; |
| link.classList.add('disabled'); |
| } |
| link.setAttribute('aria-pressed', stateText); |
| link.addEventListener( |
| 'click', |
| toggleAccessibility.bind(this, data, link, mode, globalStateName)); |
| return link; |
| } |
| |
| function createShowAccessibilityTreeElement( |
| data, id, requestType, opt_refresh) { |
| const show = document.createElement('button'); |
| if (requestType == 'showOrRefreshTree') { |
| // Give feedback that the tree has loaded. |
| show.textContent = 'Accessibility tree loaded'; |
| setTimeout(() => { |
| show.textContent = 'Refresh accessibility tree'; |
| }, 5000); |
| } else { |
| show.textContent = |
| opt_refresh ? 'Refresh accessibility tree' : 'Show accessibility tree'; |
| } |
| show.id = id + ':showOrRefreshTree'; |
| show.setAttribute('aria-expanded', String(opt_refresh)); |
| show.addEventListener('click', requestTree.bind(this, data, show)); |
| return show; |
| } |
| |
| function createHideAccessibilityTreeElement(id) { |
| const hide = document.createElement('button'); |
| hide.textContent = 'Hide accessibility tree'; |
| hide.id = id + ':hideTree'; |
| hide.addEventListener('click', function() { |
| const show = $(id + ':showOrRefreshTree'); |
| show.textContent = 'Show accessibility tree'; |
| show.setAttribute('aria-expanded', 'false'); |
| show.focus(); |
| const elements = ['hideTree', 'tree']; |
| for (let i = 0; i < elements.length; i++) { |
| const elt = $(id + ':' + elements[i]); |
| if (elt) { |
| elt.style.display = 'none'; |
| } |
| } |
| }); |
| return hide; |
| } |
| |
| function createCopyAccessibilityTreeElement(data, id) { |
| const copy = document.createElement('button'); |
| copy.textContent = 'Copy accessibility tree'; |
| copy.id = id + ':copyTree'; |
| copy.addEventListener('click', requestTree.bind(this, data, copy)); |
| return copy; |
| } |
| |
| function createStartStopAccessibilityEventRecordingElement(data, id) { |
| const show = document.createElement('button'); |
| show.classList.add('recordEventsButton'); |
| show.textContent = 'Start recording'; |
| show.id = id + ':startOrStopEvents'; |
| show.setAttribute('aria-expanded', 'false'); |
| show.addEventListener('click', requestEvents.bind(this, data, show)); |
| return show; |
| } |
| |
| function createErrorMessageElement(data) { |
| const errorMessageElement = document.createElement('div'); |
| const errorMessage = data.error; |
| const nbsp = '\u00a0'; |
| errorMessageElement.textContent = errorMessage + nbsp; |
| const closeLink = document.createElement('a'); |
| closeLink.href = '#'; |
| closeLink.textContent = '[close]'; |
| closeLink.addEventListener('click', function() { |
| const parentElement = errorMessageElement.parentElement; |
| parentElement.removeChild(errorMessageElement); |
| if (parentElement.childElementCount == 0) { |
| parentElement.parentElement.removeChild(parentElement); |
| } |
| }); |
| errorMessageElement.appendChild(closeLink); |
| return errorMessageElement; |
| } |
| |
| // WebUI listener handler for the 'showOrRefreshTree' event. |
| function showOrRefreshTree(data) { |
| const id = getIdFromData(data); |
| const row = $(id); |
| if (!row) { |
| return; |
| } |
| |
| row.textContent = ''; |
| formatRow(row, data, 'showOrRefreshTree'); |
| $(id + ':showOrRefreshTree').focus(); |
| } |
| |
| // WebUI listener handler for the 'startOrStopEvents' event. |
| function startOrStopEvents(data) { |
| const id = getIdFromData(data); |
| const row = $(id); |
| if (!row) { |
| return; |
| } |
| |
| row.textContent = ''; |
| formatRow(row, data, null); |
| $(id + ':startOrStopEvents').focus(); |
| } |
| |
| // WebUI listener handler for the 'copyTree' event. |
| function copyTree(data) { |
| const id = getIdFromData(data); |
| const row = $(id); |
| if (!row) { |
| return; |
| } |
| const copy = $(id + ':copyTree'); |
| |
| if ('tree' in data) { |
| navigator.clipboard.writeText(data['tree']) |
| .then(() => { |
| copy.textContent = 'Copied to clipboard!'; |
| setTimeout(() => { |
| copy.textContent = 'Copy accessibility tree'; |
| }, 5000); |
| }) |
| .catch(err => { |
| console.error('Unable to copy accessibility tree.', err); |
| }); |
| } else if ('error' in data) { |
| console.error('Unable to copy accessibility tree.', data.error); |
| } |
| |
| const tree = $(id + ':tree'); |
| // If the tree is currently shown, update it since it may have changed. |
| if (tree && tree.style.display != 'none') { |
| showOrRefreshTree(data); |
| $(id + ':copyTree').focus(); |
| } |
| } |
| |
| function createNativeUITreeElement(browser) { |
| const id = 'browser.' + browser.id; |
| const row = document.createElement('div'); |
| row.className = 'row'; |
| row.id = id; |
| formatRow(row, browser, null); |
| return row; |
| } |
| |
| // type is either 'tree' or 'eventLogs' |
| function createAccessibilityOutputElement(data, id, type) { |
| let treeElement = $(id + ':' + type); |
| if (!treeElement) { |
| treeElement = document.createElement('pre'); |
| treeElement.id = id + ':' + type; |
| } |
| const dataSplitByLine = data[type].split(/\n/); |
| for (let i = 0; i < dataSplitByLine.length; i++) { |
| const lineElement = document.createElement('div'); |
| lineElement.textContent = dataSplitByLine[i]; |
| treeElement.appendChild(lineElement); |
| } |
| return treeElement; |
| } |
| |
| document.addEventListener('DOMContentLoaded', initialize); |