braille_util.js revision 46d4c2bc3267f3f028f39e7e311b0f89aba2e4fd
1// Copyright 2014 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5/** 6 * @fileoverview A utility class for general braille functionality. 7 */ 8 9 10goog.provide('cvox.BrailleUtil'); 11 12goog.require('cvox.ChromeVox'); 13goog.require('cvox.DomUtil'); 14goog.require('cvox.Focuser'); 15goog.require('cvox.NavBraille'); 16goog.require('cvox.NodeStateUtil'); 17goog.require('cvox.Spannable'); 18 19 20/** 21 * Trimmable whitespace character that appears between consecutive items in 22 * braille. 23 * @const {string} 24 */ 25cvox.BrailleUtil.ITEM_SEPARATOR = ' '; 26 27 28/** 29 * Messages considered as containers in braille. 30 * Containers are distinguished from roles by their appearance higher up in the 31 * DOM tree of a selected node. 32 * This list should be very short. 33 * @type {!Array.<string>} 34 */ 35cvox.BrailleUtil.CONTAINER = [ 36 'tag_h1_brl', 37 'tag_h2_brl', 38 'tag_h3_brl', 39 'tag_h4_brl', 40 'tag_h5_brl', 41 'tag_h6_brl' 42]; 43 44 45/** 46 * Maps a ChromeVox message id to a braille template. 47 * The template takes one-character specifiers: 48 * n: replaced with braille name. 49 * r: replaced with braille role. 50 * s: replaced with braille state. 51 * c: replaced with braille container role; this potentially returns whitespace, 52 * so place at the beginning or end of templates for trimming. 53 * v: replaced with braille value. 54 * @type {Object.<string, string>} 55 */ 56cvox.BrailleUtil.TEMPLATE = { 57 'base': 'c n v r s', 58 'aria_role_alert': 'r: n', 59 'aria_role_button': '[n]', 60 'aria_role_textbox': 'n: v r', 61 'input_type_button': '[n]', 62 'input_type_checkbox': 'n (s)', 63 'input_type_email': 'n: v r', 64 'input_type_number': 'n: v r', 65 'input_type_password': 'n: v r', 66 'input_type_search': 'n: v r', 67 'input_type_submit': '[n]', 68 'input_type_text': 'n: v r', 69 'input_type_tel': 'n: v r', 70 'input_type_url': 'n: v r', 71 'tag_button': '[n]', 72 'tag_textarea': 'n: v r' 73}; 74 75 76/** 77 * Attached to the value region of a braille spannable. 78 * @param {number} offset The offset of the span into the value. 79 * @constructor 80 * @struct 81 */ 82cvox.BrailleUtil.ValueSpan = function(offset) { 83 /** 84 * The offset of the span into the value. 85 * @type {number} 86 */ 87 this.offset = offset; 88}; 89 90 91/** 92 * Creates a value span from a json serializable object. 93 * @param {!Object} obj The json serializable object to convert. 94 * @return {!cvox.BrailleUtil.ValueSpan} 95 */ 96cvox.BrailleUtil.ValueSpan.fromJson = function(obj) { 97 return new cvox.BrailleUtil.ValueSpan(obj.offset); 98}; 99 100 101/** 102 * Converts this object to a json serializable object. 103 * @return {!Object} 104 */ 105cvox.BrailleUtil.ValueSpan.prototype.toJson = function() { 106 return this; 107}; 108 109 110cvox.Spannable.registerSerializableSpan( 111 cvox.BrailleUtil.ValueSpan, 112 'cvox.BrailleUtil.ValueSpan', 113 cvox.BrailleUtil.ValueSpan.fromJson, 114 cvox.BrailleUtil.ValueSpan.prototype.toJson); 115 116 117/** 118 * Attached to the selected text within a value. 119 * @constructor 120 * @struct 121 */ 122cvox.BrailleUtil.ValueSelectionSpan = function() { 123}; 124 125 126cvox.Spannable.registerStatelessSerializableSpan( 127 cvox.BrailleUtil.ValueSelectionSpan, 128 'cvox.BrailleUtil.ValueSelectionSpan'); 129 130 131/** 132 * Gets the braille name for a node. 133 * See DomUtil for a more precise definition of 'name'. 134 * Additionally, whitespace is trimmed. 135 * @param {Node} node The node. 136 * @return {string} The string representation. 137 */ 138cvox.BrailleUtil.getName = function(node) { 139 if (!node) { 140 return ''; 141 } 142 return cvox.DomUtil.getName(node).trim(); 143}; 144 145 146/** 147 * Gets the braille role message id for a node. 148 * See DomUtil for a more precise definition of 'role'. 149 * @param {Node} node The node. 150 * @return {string} The string representation. 151 */ 152cvox.BrailleUtil.getRoleMsg = function(node) { 153 if (!node) { 154 return ''; 155 } 156 var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE); 157 if (roleMsg) { 158 roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg); 159 } 160 if (roleMsg && (roleMsg.length > 0)) { 161 if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) { 162 roleMsg += '_brl'; 163 } 164 } 165 return roleMsg; 166}; 167 168 169/** 170 * Gets the braille role of a node. 171 * See DomUtil for a more precise definition of 'role'. 172 * @param {Node} node The node. 173 * @return {string} The string representation. 174 */ 175cvox.BrailleUtil.getRole = function(node) { 176 if (!node) { 177 return ''; 178 } 179 var roleMsg = cvox.BrailleUtil.getRoleMsg(node); 180 return roleMsg ? cvox.ChromeVox.msgs.getMsg(roleMsg) : ''; 181}; 182 183 184/** 185 * Gets the braille state of a node. 186 * @param {Node} node The node. 187 * @return {string} The string representation. 188 */ 189cvox.BrailleUtil.getState = function(node) { 190 if (!node) { 191 return ''; 192 } 193 return cvox.NodeStateUtil.expand( 194 cvox.DomUtil.getStateMsgs(node, true).map(function(state) { 195 // Check to see if a variant of the message with '_brl' exists, 196 // and use it if so. 197 // 198 // Note: many messages are templatized, and if we don't pass any 199 // argument to substitute, getMsg might throw an error if the 200 // resulting string is empty. To avoid this, we pass a dummy 201 // substitution string array here. 202 var dummySubs = ['dummy', 'dummy', 'dummy']; 203 if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) { 204 state[0] += '_brl'; 205 } 206 return state; 207 })); 208}; 209 210 211/** 212 * Gets the braille container role of a node. 213 * @param {Node} prev The previous node in navigation. 214 * @param {Node} node The node. 215 * @return {string} The string representation. 216 */ 217cvox.BrailleUtil.getContainer = function(prev, node) { 218 if (!prev || !node) { 219 return ''; 220 } 221 var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node); 222 for (var i = 0, container; container = ancestors[i]; i++) { 223 var msg = cvox.BrailleUtil.getRoleMsg(container); 224 if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) { 225 return cvox.ChromeVox.msgs.getMsg(msg); 226 } 227 } 228 return ''; 229}; 230 231 232/** 233 * Gets the braille value of a node. A cvox.BrailleUtil.ValueSpan will be 234 * attached, along with (possibly) a cvox.BrailleUtil.ValueSelectionSpan. 235 * @param {Node} node The node. 236 * @return {!cvox.Spannable} The value spannable. 237 */ 238cvox.BrailleUtil.getValue = function(node) { 239 if (!node) { 240 return new cvox.Spannable(); 241 } 242 var valueSpan = new cvox.BrailleUtil.ValueSpan(0 /* offset */); 243 if (cvox.DomUtil.isInputTypeText(node)) { 244 var value = node.value; 245 if (node.type === 'password') { 246 value = value.replace(/./g, '*'); 247 } 248 var spannable = new cvox.Spannable(value, valueSpan); 249 if (node === document.activeElement && 250 cvox.DomUtil.doesInputSupportSelection(node)) { 251 var selectionStart = cvox.BrailleUtil.clamp_( 252 node.selectionStart, 0, spannable.getLength()); 253 var selectionEnd = cvox.BrailleUtil.clamp_( 254 node.selectionEnd, 0, spannable.getLength()); 255 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 256 Math.min(selectionStart, selectionEnd), 257 Math.max(selectionStart, selectionEnd)); 258 } 259 return spannable; 260 } else if (node instanceof HTMLTextAreaElement) { 261 var shadow = new cvox.EditableTextAreaShadow(); 262 shadow.update(node); 263 var lineIndex = shadow.getLineIndex(node.selectionEnd); 264 var lineStart = shadow.getLineStart(lineIndex); 265 var lineEnd = shadow.getLineEnd(lineIndex); 266 var lineText = node.value.substring(lineStart, lineEnd); 267 valueSpan.offset = lineStart; 268 var spannable = new cvox.Spannable(lineText, valueSpan); 269 if (node === document.activeElement) { 270 var selectionStart = cvox.BrailleUtil.clamp_( 271 node.selectionStart - lineStart, 0, spannable.getLength()); 272 var selectionEnd = cvox.BrailleUtil.clamp_( 273 node.selectionEnd - lineStart, 0, spannable.getLength()); 274 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 275 Math.min(selectionStart, selectionEnd), 276 Math.max(selectionStart, selectionEnd)); 277 } 278 return spannable; 279 } else { 280 return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan); 281 } 282}; 283 284 285/** 286 * Gets the templated representation of braille. 287 * @param {Node} prev The previous node (during navigation). 288 * @param {Node} node The node. 289 * @param {{name:(undefined|string), 290 * role:(undefined|string), 291 * roleMsg:(undefined|string), 292 * state:(undefined|string), 293 * container:(undefined|string), 294 * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a 295 * specific property for the given node. 296 * @return {!cvox.Spannable} The string representation. 297 */ 298cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) { 299 opt_override = opt_override ? opt_override : {}; 300 var roleMsg = opt_override.roleMsg || 301 (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : ''); 302 var role = opt_override.role; 303 if (!role && opt_override.roleMsg) { 304 role = cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg + '_brl') || 305 cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg); 306 } 307 role = role || cvox.BrailleUtil.getRole(node); 308 var template = cvox.BrailleUtil.TEMPLATE[roleMsg] || 309 cvox.BrailleUtil.TEMPLATE['base']; 310 311 var templated = new cvox.Spannable(); 312 var mapChar = function(c) { 313 switch (c) { 314 case 'n': 315 return opt_override.name || cvox.BrailleUtil.getName(node); 316 case 'r': 317 return role; 318 case 's': 319 return opt_override.state || cvox.BrailleUtil.getState(node); 320 case 'c': 321 return opt_override.container || 322 cvox.BrailleUtil.getContainer(prev, node); 323 case 'v': 324 return opt_override.value || cvox.BrailleUtil.getValue(node); 325 default: 326 return c; 327 } 328 }; 329 for (var i = 0; i < template.length; i++) { 330 var component = mapChar(template[i]); 331 templated.append(component); 332 // Ignore the next whitespace separator if the current component is empty. 333 if (!component.toString() && template[i + 1] == ' ') { 334 i++; 335 } 336 } 337 return templated.trimRight(); 338}; 339 340 341/** 342 * Creates a braille value from a string and, optionally, a selection range. 343 * A cvox.BrailleUtil.ValueSpan will be 344 * attached, along with a cvox.BrailleUtil.ValueSelectionSpan if applicable. 345 * @param {string} text The text to display as the value. 346 * @param {number=} opt_selStart Selection start. 347 * @param {number=} opt_selEnd Selection end if different from selection start. 348 * @param {number=} opt_textOffset Start offset of text. 349 * @return {!cvox.Spannable} The value spannable. 350 */ 351cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd, 352 opt_textOffset) { 353 var spannable = new cvox.Spannable( 354 text, new cvox.BrailleUtil.ValueSpan(opt_textOffset || 0)); 355 if (goog.isDef(opt_selStart)) { 356 opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart; 357 // TODO(plundblad): This looses the distinction between the selection 358 // anchor (start) and focus (end). We should use that information to 359 // decide where to pan the braille display. 360 if (opt_selStart > opt_selEnd) { 361 var temp = opt_selStart; 362 opt_selStart = opt_selEnd; 363 opt_selEnd = temp; 364 } 365 366 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 367 opt_selStart, opt_selEnd); 368 } 369 return spannable; 370}; 371 372 373/** 374 * Activates a position in a nav braille. Moves the caret in text fields 375 * and simulates a mouse click on the node at the position. 376 * 377 * @param {!cvox.NavBraille} braille the nav braille representing the display 378 * content that was active when the user issued the key command. 379 * The annotations in the spannable are used to decide what 380 * node to activate and what part of the node value (if any) to 381 * move the caret to. 382 * @param {number=} opt_displayPosition position of the display that the user 383 * activated, relative to the start of braille. 384 */ 385cvox.BrailleUtil.click = function(braille, opt_displayPosition) { 386 var spans = braille.text.getSpans(opt_displayPosition || 0); 387 var node = spans.filter(function(n) { return n instanceof Node; })[0]; 388 if (node) { 389 cvox.Focuser.setFocus(node); 390 if (goog.isDef(opt_displayPosition) && 391 (cvox.DomUtil.isInputTypeText(node) || 392 node instanceof HTMLTextAreaElement)) { 393 var valueSpan = spans.filter( 394 function(s) { 395 return s instanceof cvox.BrailleUtil.ValueSpan; 396 })[0]; 397 if (valueSpan) { 398 var cursorPosition = opt_displayPosition - 399 braille.text.getSpanStart(valueSpan) + 400 valueSpan.offset; 401 cvox.ChromeVoxEventWatcher.setUpTextHandler(); 402 node.selectionStart = node.selectionEnd = cursorPosition; 403 cvox.ChromeVoxEventWatcher.handleTextChanged(true); 404 } 405 } 406 } 407 cvox.DomUtil.clickElem(node || 408 cvox.ChromeVox.navigationManager.getCurrentNode(), false, false); 409}; 410 411 412/** 413 * Clamps a number so it is within the given boundaries. 414 * @param {number} number The number to clamp. 415 * @param {number} min The minimum value to return. 416 * @param {number} max The maximum value to return. 417 * @return {number} {@code number} if it is within the bounds, or the nearest 418 * number within the bounds otherwise. 419 * @private 420 */ 421cvox.BrailleUtil.clamp_ = function(number, min, max) { 422 return Math.min(Math.max(number, min), max); 423}; 424