// Copyright 2014 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. /** * @fileoverview A utility class for general braille functionality. */ goog.provide('cvox.BrailleUtil'); goog.require('cvox.ChromeVox'); goog.require('cvox.DomUtil'); goog.require('cvox.EditableTextAreaShadow'); goog.require('cvox.Focuser'); goog.require('cvox.NavBraille'); goog.require('cvox.NodeStateUtil'); goog.require('cvox.Spannable'); goog.require('cvox.ValueSelectionSpan'); goog.require('cvox.ValueSpan'); /** * Trimmable whitespace character that appears between consecutive items in * braille. * @const {string} */ cvox.BrailleUtil.ITEM_SEPARATOR = ' '; /** * Messages considered as containers in braille. * Containers are distinguished from roles by their appearance higher up in the * DOM tree of a selected node. * This list should be very short. * @type {!Array} */ cvox.BrailleUtil.CONTAINER = [ 'tag_h1_brl', 'tag_h2_brl', 'tag_h3_brl', 'tag_h4_brl', 'tag_h5_brl', 'tag_h6_brl' ]; /** * Maps a ChromeVox message id to a braille template. * The template takes one-character specifiers: * n: replaced with braille name. * r: replaced with braille role. * s: replaced with braille state. * c: replaced with braille container role; this potentially returns whitespace, * so place at the beginning or end of templates for trimming. * v: replaced with braille value. * @type {Object} */ cvox.BrailleUtil.TEMPLATE = { 'base': 'c n v r s', 'aria_role_alert': 'r: n', 'aria_role_button': 'n r s', 'aria_role_checkbox': 'n r (s)', 'aria_role_menuitemcheckbox': 'n r (s)', 'aria_role_menuitemradio': 'n r (s)', 'aria_role_radio': 'n r (s)', 'aria_role_textbox': 'n: v r s', 'input_type_button': 'n r s', 'input_type_checkbox': 'n r (s)', 'input_type_email': 'n: v r s', 'input_type_number': 'n: v r s', 'input_type_password': 'n: v r s', 'input_type_radio': 'n r (s)', 'input_type_search': 'n: v r s', 'input_type_submit': 'n r s', 'input_type_text': 'n: v r s', 'input_type_tel': 'n: v r s', 'input_type_url': 'n: v r s', 'tag_button': 'n r s', 'tag_textarea': 'n: v r s' }; /** * Gets the braille name for a node. * See DomUtil for a more precise definition of 'name'. * Additionally, whitespace is trimmed. * @param {Node} node The node. * @return {string} The string representation. */ cvox.BrailleUtil.getName = function(node) { if (!node) { return ''; } return cvox.DomUtil.getName(node).trim(); }; /** * Gets the braille role message id for a node. * See DomUtil for a more precise definition of 'role'. * @param {Node} node The node. * @return {string} The string representation. */ cvox.BrailleUtil.getRoleMsg = function(node) { if (!node) { return ''; } var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE); if (roleMsg) { roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg); } if (roleMsg && (roleMsg.length > 0)) { if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) { roleMsg += '_brl'; } } return roleMsg; }; /** * Transforms a {@code cvox.NodeState} list of state messages to the * corresponding messages for braille and expands them into a localized * string suitable for output on a braille display. * @param {cvox.NodeState} stateMsgs The states to expand. The content of this * array is modified. * @return {string} The string representation. * @private */ cvox.BrailleUtil.expandStateMsgs_ = function(stateMsgs) { stateMsgs.forEach(function(state) { // Check to see if a variant of the message with '_brl' exists, // and use it if so. // // Note: many messages are templatized, and if we don't pass any // argument to substitute, getMsg might throw an error if the // resulting string is empty. To avoid this, we pass a dummy // substitution string array here. var dummySubs = ['dummy', 'dummy', 'dummy']; if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) { state[0] += '_brl'; } }); return cvox.NodeStateUtil.expand(stateMsgs); }; /** * Gets the braille container role of a node. * @param {Node} prev The previous node in navigation. * @param {Node} node The node. * @return {string} The string representation. */ cvox.BrailleUtil.getContainer = function(prev, node) { if (!prev || !node) { return ''; } var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node); for (var i = 0, container; container = ancestors[i]; i++) { var msg = cvox.BrailleUtil.getRoleMsg(container); if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) { return cvox.ChromeVox.msgs.getMsg(msg); } } return ''; }; /** * Gets the braille value of a node. A {@code cvox.ValueSpan} will be * attached, along with (possibly) a {@code cvox.ValueSelectionSpan}. * @param {Node} node The node. * @return {!cvox.Spannable} The value spannable. */ cvox.BrailleUtil.getValue = function(node) { if (!node) { return new cvox.Spannable(); } var valueSpan = new cvox.ValueSpan(0 /* offset */); if (cvox.DomUtil.isInputTypeText(node)) { var value = node.value; if (node.type === 'password') { value = value.replace(/./g, '*'); } var spannable = new cvox.Spannable(value, valueSpan); if (node === document.activeElement && cvox.DomUtil.doesInputSupportSelection(node)) { var selectionStart = cvox.BrailleUtil.clamp_( node.selectionStart, 0, spannable.getLength()); var selectionEnd = cvox.BrailleUtil.clamp_( node.selectionEnd, 0, spannable.getLength()); spannable.setSpan(new cvox.ValueSelectionSpan(), Math.min(selectionStart, selectionEnd), Math.max(selectionStart, selectionEnd)); } return spannable; } else if (node instanceof HTMLTextAreaElement) { var shadow = new cvox.EditableTextAreaShadow(); shadow.update(node); var lineIndex = shadow.getLineIndex(node.selectionEnd); var lineStart = shadow.getLineStart(lineIndex); var lineEnd = shadow.getLineEnd(lineIndex); var lineText = node.value.substring(lineStart, lineEnd); valueSpan.offset = lineStart; var spannable = new cvox.Spannable(lineText, valueSpan); if (node === document.activeElement) { var selectionStart = cvox.BrailleUtil.clamp_( node.selectionStart - lineStart, 0, spannable.getLength()); var selectionEnd = cvox.BrailleUtil.clamp_( node.selectionEnd - lineStart, 0, spannable.getLength()); spannable.setSpan(new cvox.ValueSelectionSpan(), Math.min(selectionStart, selectionEnd), Math.max(selectionStart, selectionEnd)); } return spannable; } else { return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan); } }; /** * Gets the templated representation of braille. * @param {Node} prev The previous node (during navigation). * @param {Node} node The node. * @param {{name:(undefined|string), * role:(undefined|string), * roleMsg:(undefined|string), * state:(undefined|string), * container:(undefined|string), * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a * specific property for the given node. * @return {!cvox.Spannable} The string representation. */ cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) { opt_override = opt_override ? opt_override : {}; var roleMsg = opt_override.roleMsg || (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : ''); var template = cvox.BrailleUtil.TEMPLATE[roleMsg] || cvox.BrailleUtil.TEMPLATE['base']; var state = opt_override.state; if (!state) { if (node) { state = cvox.BrailleUtil.expandStateMsgs_( cvox.DomUtil.getStateMsgs(node, true)); } else { state = ''; } } var role = opt_override.role || ''; if (!role && roleMsg) { role = cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl') || cvox.ChromeVox.msgs.getMsg(roleMsg); } var templated = new cvox.Spannable(); var mapChar = function(c) { switch (c) { case 'n': return opt_override.name || cvox.BrailleUtil.getName(node); case 'r': return role; case 's': return state; case 'c': return opt_override.container || cvox.BrailleUtil.getContainer(prev, node); case 'v': return opt_override.value || cvox.BrailleUtil.getValue(node); default: return c; } }; for (var i = 0; i < template.length; i++) { var component = mapChar(template[i]); templated.append(component); // Ignore the next whitespace separator if the current component is empty, // unless the empty value has a selection, in which case the cursor // should be placed on the empty space after the empty value. if (!component.toString() && template[i + 1] == ' ' && (!(component instanceof cvox.Spannable) || !/**@type {cvox.Spannable}*/(component).getSpanInstanceOf( cvox.ValueSelectionSpan))) { i++; } } return templated.trimRight(); }; /** * Creates a braille value from a string and, optionally, a selection range. * A {@code cvox.ValueSpan} will be attached, along with a * {@code cvox.ValueSelectionSpan} if applicable. * @param {string} text The text to display as the value. * @param {number=} opt_selStart Selection start. * @param {number=} opt_selEnd Selection end if different from selection start. * @param {number=} opt_textOffset Start offset of text. * @return {!cvox.Spannable} The value spannable. */ cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd, opt_textOffset) { var spannable = new cvox.Spannable( text, new cvox.ValueSpan(opt_textOffset || 0)); if (goog.isDef(opt_selStart)) { opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart; // TODO(plundblad): This looses the distinction between the selection // anchor (start) and focus (end). We should use that information to // decide where to pan the braille display. if (opt_selStart > opt_selEnd) { var temp = opt_selStart; opt_selStart = opt_selEnd; opt_selEnd = temp; } spannable.setSpan(new cvox.ValueSelectionSpan(), opt_selStart, opt_selEnd); } return spannable; }; /** * Activates a position in a nav braille. Moves the caret in text fields * and simulates a mouse click on the node at the position. * * @param {!cvox.NavBraille} braille the nav braille representing the display * content that was active when the user issued the key command. * The annotations in the spannable are used to decide what * node to activate and what part of the node value (if any) to * move the caret to. * @param {number=} opt_displayPosition position of the display that the user * activated, relative to the start of braille. */ cvox.BrailleUtil.click = function(braille, opt_displayPosition) { var handled = false; var spans = braille.text.getSpans(opt_displayPosition || 0); var node = spans.filter(function(n) { return n instanceof Node; })[0]; if (node) { if (goog.isDef(opt_displayPosition) && (cvox.DomUtil.isInputTypeText(node) || node instanceof HTMLTextAreaElement)) { var valueSpan = spans.filter( function(s) { return s instanceof cvox.ValueSpan; })[0]; if (valueSpan) { if (document.activeElement !== node) { cvox.Focuser.setFocus(node); } var cursorPosition = opt_displayPosition - braille.text.getSpanStart(valueSpan) + valueSpan.offset; cvox.ChromeVoxEventWatcher.setUpTextHandler(); node.selectionStart = node.selectionEnd = cursorPosition; cvox.ChromeVoxEventWatcher.handleTextChanged(true); handled = true; } } } if (!handled) { cvox.DomUtil.clickElem( node || cvox.ChromeVox.navigationManager.getCurrentNode(), false, false, false, true); } }; /** * Clamps a number so it is within the given boundaries. * @param {number} number The number to clamp. * @param {number} min The minimum value to return. * @param {number} max The maximum value to return. * @return {number} {@code number} if it is within the bounds, or the nearest * number within the bounds otherwise. * @private */ cvox.BrailleUtil.clamp_ = function(number, min, max) { return Math.min(Math.max(number, min), max); };