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 */ 81cvox.BrailleUtil.ValueSpan = function(offset) { 82 /** 83 * The offset of the span into the value. 84 * @type {number} 85 */ 86 this.offset = offset; 87}; 88 89 90/** 91 * Creates a value span from a json serializable object. 92 * @param {!Object} obj The json serializable object to convert. 93 * @return {!cvox.BrailleUtil.ValueSpan} The value span. 94 */ 95cvox.BrailleUtil.ValueSpan.fromJson = function(obj) { 96 return new cvox.BrailleUtil.ValueSpan(obj.offset); 97}; 98 99 100/** 101 * Converts this object to a json serializable object. 102 * @return {!Object} The JSON representation. 103 */ 104cvox.BrailleUtil.ValueSpan.prototype.toJson = function() { 105 return this; 106}; 107 108 109cvox.Spannable.registerSerializableSpan( 110 cvox.BrailleUtil.ValueSpan, 111 'cvox.BrailleUtil.ValueSpan', 112 cvox.BrailleUtil.ValueSpan.fromJson, 113 cvox.BrailleUtil.ValueSpan.prototype.toJson); 114 115 116/** 117 * Attached to the selected text within a value. 118 * @constructor 119 */ 120cvox.BrailleUtil.ValueSelectionSpan = function() { 121}; 122 123 124cvox.Spannable.registerStatelessSerializableSpan( 125 cvox.BrailleUtil.ValueSelectionSpan, 126 'cvox.BrailleUtil.ValueSelectionSpan'); 127 128 129/** 130 * Gets the braille name for a node. 131 * See DomUtil for a more precise definition of 'name'. 132 * Additionally, whitespace is trimmed. 133 * @param {Node} node The node. 134 * @return {string} The string representation. 135 */ 136cvox.BrailleUtil.getName = function(node) { 137 if (!node) { 138 return ''; 139 } 140 return cvox.DomUtil.getName(node).trim(); 141}; 142 143 144/** 145 * Gets the braille role message id for a node. 146 * See DomUtil for a more precise definition of 'role'. 147 * @param {Node} node The node. 148 * @return {string} The string representation. 149 */ 150cvox.BrailleUtil.getRoleMsg = function(node) { 151 if (!node) { 152 return ''; 153 } 154 var roleMsg = cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE); 155 if (roleMsg) { 156 roleMsg = cvox.DomUtil.collapseWhitespace(roleMsg); 157 } 158 if (roleMsg && (roleMsg.length > 0)) { 159 if (cvox.ChromeVox.msgs.getMsg(roleMsg + '_brl')) { 160 roleMsg += '_brl'; 161 } 162 } 163 return roleMsg; 164}; 165 166 167/** 168 * Gets the braille role of a node. 169 * See DomUtil for a more precise definition of 'role'. 170 * @param {Node} node The node. 171 * @return {string} The string representation. 172 */ 173cvox.BrailleUtil.getRole = function(node) { 174 if (!node) { 175 return ''; 176 } 177 var roleMsg = cvox.BrailleUtil.getRoleMsg(node); 178 return roleMsg ? cvox.ChromeVox.msgs.getMsg(roleMsg) : ''; 179}; 180 181 182/** 183 * Gets the braille state of a node. 184 * @param {Node} node The node. 185 * @return {string} The string representation. 186 */ 187cvox.BrailleUtil.getState = function(node) { 188 if (!node) { 189 return ''; 190 } 191 return cvox.NodeStateUtil.expand( 192 cvox.DomUtil.getStateMsgs(node, true).map(function(state) { 193 // Check to see if a variant of the message with '_brl' exists, 194 // and use it if so. 195 // 196 // Note: many messages are templatized, and if we don't pass any 197 // argument to substitute, getMsg might throw an error if the 198 // resulting string is empty. To avoid this, we pass a dummy 199 // substitution string array here. 200 var dummySubs = ['dummy', 'dummy', 'dummy']; 201 if (cvox.ChromeVox.msgs.getMsg(state[0] + '_brl', dummySubs)) { 202 state[0] += '_brl'; 203 } 204 return state; 205 })); 206}; 207 208 209/** 210 * Gets the braille container role of a node. 211 * @param {Node} prev The previous node in navigation. 212 * @param {Node} node The node. 213 * @return {string} The string representation. 214 */ 215cvox.BrailleUtil.getContainer = function(prev, node) { 216 if (!prev || !node) { 217 return ''; 218 } 219 var ancestors = cvox.DomUtil.getUniqueAncestors(prev, node); 220 for (var i = 0, container; container = ancestors[i]; i++) { 221 var msg = cvox.BrailleUtil.getRoleMsg(container); 222 if (msg && cvox.BrailleUtil.CONTAINER.indexOf(msg) != -1) { 223 return cvox.ChromeVox.msgs.getMsg(msg); 224 } 225 } 226 return ''; 227}; 228 229 230/** 231 * Gets the braille value of a node. A cvox.BrailleUtil.ValueSpan will be 232 * attached, along with (possibly) a cvox.BrailleUtil.ValueSelectionSpan. 233 * @param {Node} node The node. 234 * @return {!cvox.Spannable} The value spannable. 235 */ 236cvox.BrailleUtil.getValue = function(node) { 237 if (!node) { 238 return new cvox.Spannable(); 239 } 240 var valueSpan = new cvox.BrailleUtil.ValueSpan(0 /* offset */); 241 if (cvox.DomUtil.isInputTypeText(node)) { 242 var value = node.value; 243 if (node.type === 'password') { 244 value = value.replace(/./g, '*'); 245 } 246 var spannable = new cvox.Spannable(value, valueSpan); 247 if (node === document.activeElement && 248 cvox.DomUtil.doesInputSupportSelection(node)) { 249 var selectionStart = cvox.BrailleUtil.clamp_( 250 node.selectionStart, 0, spannable.getLength()); 251 var selectionEnd = cvox.BrailleUtil.clamp_( 252 node.selectionEnd, 0, spannable.getLength()); 253 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 254 Math.min(selectionStart, selectionEnd), 255 Math.max(selectionStart, selectionEnd)); 256 } 257 return spannable; 258 } else if (node instanceof HTMLTextAreaElement) { 259 var shadow = new cvox.EditableTextAreaShadow(); 260 shadow.update(node); 261 var lineIndex = shadow.getLineIndex(node.selectionEnd); 262 var lineStart = shadow.getLineStart(lineIndex); 263 var lineEnd = shadow.getLineEnd(lineIndex); 264 var lineText = node.value.substring(lineStart, lineEnd); 265 valueSpan.offset = lineStart; 266 var spannable = new cvox.Spannable(lineText, valueSpan); 267 if (node === document.activeElement) { 268 var selectionStart = cvox.BrailleUtil.clamp_( 269 node.selectionStart - lineStart, 0, spannable.getLength()); 270 var selectionEnd = cvox.BrailleUtil.clamp_( 271 node.selectionEnd - lineStart, 0, spannable.getLength()); 272 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 273 Math.min(selectionStart, selectionEnd), 274 Math.max(selectionStart, selectionEnd)); 275 } 276 return spannable; 277 } else { 278 return new cvox.Spannable(cvox.DomUtil.getValue(node), valueSpan); 279 } 280}; 281 282 283/** 284 * Gets the templated representation of braille. 285 * @param {Node} prev The previous node (during navigation). 286 * @param {Node} node The node. 287 * @param {{name:(undefined|string), 288 * role:(undefined|string), 289 * roleMsg:(undefined|string), 290 * state:(undefined|string), 291 * container:(undefined|string), 292 * value:(undefined|cvox.Spannable)}|Object=} opt_override Override a 293 * specific property for the given node. 294 * @return {!cvox.Spannable} The string representation. 295 */ 296cvox.BrailleUtil.getTemplated = function(prev, node, opt_override) { 297 opt_override = opt_override ? opt_override : {}; 298 var roleMsg = opt_override.roleMsg || 299 (node ? cvox.DomUtil.getRoleMsg(node, cvox.VERBOSITY_VERBOSE) : ''); 300 var role = opt_override.role; 301 if (!role && opt_override.roleMsg) { 302 role = cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg + '_brl') || 303 cvox.ChromeVox.msgs.getMsg(opt_override.roleMsg); 304 } 305 role = role || cvox.BrailleUtil.getRole(node); 306 var template = cvox.BrailleUtil.TEMPLATE[roleMsg] || 307 cvox.BrailleUtil.TEMPLATE['base']; 308 309 var templated = new cvox.Spannable(); 310 var mapChar = function(c) { 311 switch (c) { 312 case 'n': 313 return opt_override.name || cvox.BrailleUtil.getName(node); 314 case 'r': 315 return role; 316 case 's': 317 return opt_override.state || cvox.BrailleUtil.getState(node); 318 case 'c': 319 return opt_override.container || 320 cvox.BrailleUtil.getContainer(prev, node); 321 case 'v': 322 return opt_override.value || cvox.BrailleUtil.getValue(node); 323 default: 324 return c; 325 } 326 }; 327 for (var i = 0; i < template.length; i++) { 328 var component = mapChar(template[i]); 329 templated.append(component); 330 // Ignore the next whitespace separator if the current component is empty. 331 if (!component.toString() && template[i + 1] == ' ') { 332 i++; 333 } 334 } 335 return templated.trimRight(); 336}; 337 338 339/** 340 * Creates a braille value from a string and, optionally, a selection range. 341 * A cvox.BrailleUtil.ValueSpan will be 342 * attached, along with a cvox.BrailleUtil.ValueSelectionSpan if applicable. 343 * @param {string} text The text to display as the value. 344 * @param {number=} opt_selStart Selection start. 345 * @param {number=} opt_selEnd Selection end if different from selection start. 346 * @param {number=} opt_textOffset Start offset of text. 347 * @return {!cvox.Spannable} The value spannable. 348 */ 349cvox.BrailleUtil.createValue = function(text, opt_selStart, opt_selEnd, 350 opt_textOffset) { 351 var spannable = new cvox.Spannable( 352 text, new cvox.BrailleUtil.ValueSpan(opt_textOffset || 0)); 353 if (goog.isDef(opt_selStart)) { 354 opt_selEnd = goog.isDef(opt_selEnd) ? opt_selEnd : opt_selStart; 355 // TODO(plundblad): This looses the distinction between the selection 356 // anchor (start) and focus (end). We should use that information to 357 // decide where to pan the braille display. 358 if (opt_selStart > opt_selEnd) { 359 var temp = opt_selStart; 360 opt_selStart = opt_selEnd; 361 opt_selEnd = temp; 362 } 363 364 spannable.setSpan(new cvox.BrailleUtil.ValueSelectionSpan(), 365 opt_selStart, opt_selEnd); 366 } 367 return spannable; 368}; 369 370 371/** 372 * Activates a position in a nav braille. Moves the caret in text fields 373 * and simulates a mouse click on the node at the position. 374 * 375 * @param {!cvox.NavBraille} braille the nav braille representing the display 376 * content that was active when the user issued the key command. 377 * The annotations in the spannable are used to decide what 378 * node to activate and what part of the node value (if any) to 379 * move the caret to. 380 * @param {number=} opt_displayPosition position of the display that the user 381 * activated, relative to the start of braille. 382 */ 383cvox.BrailleUtil.click = function(braille, opt_displayPosition) { 384 var handled = false; 385 var spans = braille.text.getSpans(opt_displayPosition || 0); 386 var node = spans.filter(function(n) { return n instanceof Node; })[0]; 387 if (node) { 388 if (goog.isDef(opt_displayPosition) && 389 (cvox.DomUtil.isInputTypeText(node) || 390 node instanceof HTMLTextAreaElement)) { 391 var valueSpan = spans.filter( 392 function(s) { 393 return s instanceof cvox.BrailleUtil.ValueSpan; 394 })[0]; 395 if (valueSpan) { 396 if (document.activeElement !== node) { 397 cvox.Focuser.setFocus(node); 398 } 399 var cursorPosition = opt_displayPosition - 400 braille.text.getSpanStart(valueSpan) + 401 valueSpan.offset; 402 cvox.ChromeVoxEventWatcher.setUpTextHandler(); 403 node.selectionStart = node.selectionEnd = cursorPosition; 404 cvox.ChromeVoxEventWatcher.handleTextChanged(true); 405 handled = true; 406 } 407 } 408 } 409 if (!handled) { 410 cvox.DomUtil.clickElem( 411 node || cvox.ChromeVox.navigationManager.getCurrentNode(), 412 false, false, false, true); 413 } 414}; 415 416 417/** 418 * Clamps a number so it is within the given boundaries. 419 * @param {number} number The number to clamp. 420 * @param {number} min The minimum value to return. 421 * @param {number} max The maximum value to return. 422 * @return {number} {@code number} if it is within the bounds, or the nearest 423 * number within the bounds otherwise. 424 * @private 425 */ 426cvox.BrailleUtil.clamp_ = function(number, min, max) { 427 return Math.min(Math.max(number, min), max); 428}; 429