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 building NavDescriptions from the dom. 7 */ 8 9 10goog.provide('cvox.DescriptionUtil'); 11 12goog.require('cvox.AriaUtil'); 13goog.require('cvox.AuralStyleUtil'); 14goog.require('cvox.BareObjectWalker'); 15goog.require('cvox.CursorSelection'); 16goog.require('cvox.DomUtil'); 17goog.require('cvox.EarconUtil'); 18goog.require('cvox.MathmlStore'); 19goog.require('cvox.NavDescription'); 20goog.require('cvox.SpeechRuleEngine'); 21goog.require('cvox.TraverseMath'); 22 23 24/** 25 * Lists all Node tagName's who's description is derived from its subtree. 26 * @type {Object.<string, boolean>} 27 */ 28cvox.DescriptionUtil.COLLECTION_NODE_TYPE = { 29 'H1': true, 30 'H2': true, 31 'H3': true, 32 'H4': true, 33 'H5': true, 34 'H6': true 35}; 36 37/** 38 * Get a control's complete description in the same format as if you 39 * navigated to the node. 40 * @param {Element} control A control. 41 * @param {Array.<Node>=} opt_changedAncestors The changed ancestors that will 42 * be used to determine what needs to be spoken. If this is not provided, the 43 * ancestors used to determine what needs to be spoken will just be the control 44 * itself and its surrounding control if it has one. 45 * @return {cvox.NavDescription} The description of the control. 46 */ 47cvox.DescriptionUtil.getControlDescription = 48 function(control, opt_changedAncestors) { 49 var ancestors = [control]; 50 if (opt_changedAncestors && (opt_changedAncestors.length > 0)) { 51 ancestors = opt_changedAncestors; 52 } else { 53 var surroundingControl = cvox.DomUtil.getSurroundingControl(control); 54 if (surroundingControl) { 55 ancestors = [surroundingControl, control]; 56 } 57 } 58 59 var description = cvox.DescriptionUtil.getDescriptionFromAncestors( 60 ancestors, true, cvox.VERBOSITY_VERBOSE); 61 62 // Use heuristics if the control doesn't otherwise have a name. 63 if (surroundingControl) { 64 var name = cvox.DomUtil.getName(surroundingControl); 65 if (name.length == 0) { 66 name = cvox.DomUtil.getControlLabelHeuristics(surroundingControl); 67 if (name.length > 0) { 68 description.context = name + ' ' + description.context; 69 } 70 } 71 } else { 72 var name = cvox.DomUtil.getName(control); 73 if (name.length == 0) { 74 name = cvox.DomUtil.getControlLabelHeuristics(control); 75 if (name.length > 0) { 76 description.text = cvox.DomUtil.collapseWhitespace(name); 77 } 78 } 79 var value = cvox.DomUtil.getValue(control); 80 if (value.length > 0) { 81 description.userValue = cvox.DomUtil.collapseWhitespace(value); 82 } 83 } 84 85 return description; 86}; 87 88 89/** 90 * Returns a description of a navigation from an array of changed 91 * ancestor nodes. The ancestors are in order from the highest in the 92 * tree to the lowest, i.e. ending with the current leaf node. 93 * 94 * @param {Array.<Node>} ancestorsArray An array of ancestor nodes. 95 * @param {boolean} recursive Whether or not the element's subtree should 96 * be used; true by default. 97 * @param {number} verbosity The verbosity setting. 98 * @return {cvox.NavDescription} The description of the navigation action. 99 */ 100cvox.DescriptionUtil.getDescriptionFromAncestors = function( 101 ancestorsArray, recursive, verbosity) { 102 if (typeof(recursive) === 'undefined') { 103 recursive = true; 104 } 105 var len = ancestorsArray.length; 106 var context = ''; 107 var text = ''; 108 var userValue = ''; 109 var annotation = ''; 110 var earcons = []; 111 var personality = null; 112 var hint = ''; 113 114 if (len > 0) { 115 text = cvox.DomUtil.getName(ancestorsArray[len - 1], recursive); 116 117 userValue = cvox.DomUtil.getValue(ancestorsArray[len - 1]); 118 } 119 for (var i = len - 1; i >= 0; i--) { 120 var node = ancestorsArray[i]; 121 122 hint = cvox.DomUtil.getHint(node); 123 124 // Don't speak dialogs here, they're spoken when events occur. 125 var role = node.getAttribute ? node.getAttribute('role') : null; 126 if (role == 'alertdialog') { 127 continue; 128 } 129 130 var roleText = cvox.DomUtil.getRole(node, verbosity); 131 132 // Use the ancestor closest to the target to be the personality. 133 if (!personality) { 134 personality = cvox.AuralStyleUtil.getStyleForNode(node); 135 } 136 // TODO(dtseng): Is this needed? 137 if (i < len - 1 && node.hasAttribute('role')) { 138 var name = cvox.DomUtil.getName(node, false); 139 if (name) { 140 roleText = name + ' ' + roleText; 141 } 142 } 143 if (roleText.length > 0) { 144 // Since we prioritize reading of context in reading order, only populate 145 // it for larger ancestry changes. 146 if (context.length > 0 || 147 (annotation.length > 0 && node.childElementCount > 1)) { 148 context = roleText + ' ' + cvox.DomUtil.getState(node, false) + 149 ' ' + context; 150 } else { 151 if (annotation.length > 0) { 152 annotation += 153 ' ' + roleText + ' ' + cvox.DomUtil.getState(node, true); 154 } else { 155 annotation = roleText + ' ' + cvox.DomUtil.getState(node, true); 156 } 157 } 158 } 159 var earcon = cvox.EarconUtil.getEarcon(node); 160 if (earcon != null && earcons.indexOf(earcon) == -1) { 161 earcons.push(earcon); 162 } 163 } 164 return new cvox.NavDescription({ 165 context: cvox.DomUtil.collapseWhitespace(context), 166 text: cvox.DomUtil.collapseWhitespace(text), 167 userValue: cvox.DomUtil.collapseWhitespace(userValue), 168 annotation: cvox.DomUtil.collapseWhitespace(annotation), 169 earcons: earcons, 170 personality: personality, 171 hint: cvox.DomUtil.collapseWhitespace(hint) 172 }); 173}; 174 175/** 176 * Returns a description of a navigation from an array of changed 177 * ancestor nodes. The ancestors are in order from the highest in the 178 * tree to the lowest, i.e. ending with the current leaf node. 179 * 180 * @param {Node} prevNode The previous node in navigation. 181 * @param {Node} node The current node in navigation. 182 * @param {boolean} recursive Whether or not the element's subtree should 183 * be used; true by default. 184 * @param {number} verbosity The verbosity setting. 185 * @return {!Array.<cvox.NavDescription>} The description of the navigation 186 * action. 187 */ 188cvox.DescriptionUtil.getDescriptionFromNavigation = 189 function(prevNode, node, recursive, verbosity) { 190 if (!prevNode || !node) { 191 return []; 192 } 193 194 // Specialized math descriptions. 195 if (cvox.DomUtil.isMath(node) && 196 !cvox.AriaUtil.isMath(node)) { 197 return cvox.DescriptionUtil.getMathDescription(node); 198 } 199 200 // Next, check to see if the current node is a collection type. 201 if (cvox.DescriptionUtil.COLLECTION_NODE_TYPE[node.tagName]) { 202 return cvox.DescriptionUtil.getCollectionDescription( 203 /** @type {!cvox.CursorSelection} */( 204 cvox.CursorSelection.fromNode(prevNode)), 205 /** @type {!cvox.CursorSelection} */( 206 cvox.CursorSelection.fromNode(node))); 207 } 208 209 // Now, generate a description for all other elements. 210 var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node, true); 211 var desc = cvox.DescriptionUtil.getDescriptionFromAncestors( 212 ancestors, recursive, verbosity); 213 var prevAncestors = cvox.DomUtil.getUniqueAncestors(node, prevNode); 214 if (cvox.DescriptionUtil.shouldDescribeExit_(prevAncestors)) { 215 var prevDesc = cvox.DescriptionUtil.getDescriptionFromAncestors( 216 prevAncestors, recursive, verbosity); 217 if (prevDesc.context && !desc.context) { 218 desc.context = 219 cvox.ChromeVox.msgs.getMsg('exited_container', [prevDesc.context]); 220 } 221 } 222 return [desc]; 223}; 224 225 226/** 227 * Returns an array of NavDescriptions that includes everything that would be 228 * spoken by an object walker while traversing from prevSel to sel. 229 * It also includes any necessary annotations and context about the set of 230 * descriptions. This function is here because most (currently all) walkers 231 * that iterate over non-leaf nodes need this sort of description. 232 * This is an awkward design, and should be changed in the future. 233 * @param {!cvox.CursorSelection} prevSel The previous selection. 234 * @param {!cvox.CursorSelection} sel The selection. 235 * @return {!Array.<!cvox.NavDescription>} The descriptions as described above. 236 */ 237cvox.DescriptionUtil.getCollectionDescription = function(prevSel, sel) { 238 var descriptions = cvox.DescriptionUtil.getRawDescriptions_(prevSel, sel); 239 cvox.DescriptionUtil.insertCollectionDescription_(descriptions); 240 return descriptions; 241}; 242 243 244/** 245 * Used for getting collection descriptions. 246 * @type {!cvox.BareObjectWalker} 247 * @private 248 */ 249cvox.DescriptionUtil.subWalker_ = new cvox.BareObjectWalker(); 250 251 252/** 253 * Returns the descriptions that would be gotten by an object walker. 254 * @param {!cvox.CursorSelection} prevSel The previous selection. 255 * @param {!cvox.CursorSelection} sel The selection. 256 * @return {!Array.<!cvox.NavDescription>} The descriptions. 257 * @private 258 */ 259cvox.DescriptionUtil.getRawDescriptions_ = function(prevSel, sel) { 260 // Use a object walker in non-smart mode to traverse all of the 261 // nodes inside the current smart node and return their annotations. 262 var descriptions = []; 263 264 // We want the descriptions to be in forward order whether or not the 265 // selection is reversed. 266 sel = sel.clone().setReversed(false); 267 var node = cvox.DescriptionUtil.subWalker_.sync(sel).start.node; 268 269 var prevNode = prevSel.end.node; 270 var curSel = cvox.CursorSelection.fromNode(node); 271 272 if (!curSel) { 273 return []; 274 } 275 276 while (cvox.DomUtil.isDescendantOfNode(node, sel.start.node)) { 277 var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node); 278 // Specialized math descriptions. 279 if (cvox.DomUtil.isMath(node) && 280 !cvox.AriaUtil.isMath(node)) { 281 descriptions = 282 descriptions.concat(cvox.DescriptionUtil.getMathDescription(node)); 283 } else { 284 var description = cvox.DescriptionUtil.getDescriptionFromAncestors( 285 ancestors, true, cvox.ChromeVox.verbosity); 286 descriptions.push(description); 287 } 288 curSel = cvox.DescriptionUtil.subWalker_.next(curSel); 289 if (!curSel) { 290 break; 291 } 292 293 curSel = /** @type {!cvox.CursorSelection} */ (curSel); 294 prevNode = node; 295 node = curSel.start.node; 296 } 297 298 return descriptions; 299}; 300 301/** 302 * Returns the full descriptions of the child nodes that would be gotten by an 303 * object walker. 304 * @param {?Element} prevnode The previous element if there is one. 305 * @param {!Element} node The target element. 306 * @return {!Array.<!cvox.NavDescription>} The descriptions. 307 */ 308cvox.DescriptionUtil.getFullDescriptionsFromChildren = 309 function(prevnode, node) { 310 var descriptions = []; 311 if (!node) { 312 return descriptions; 313 } 314 var desc; 315 if (cvox.DomUtil.isLeafNode(node)) { 316 var ancestors; 317 if (prevnode) { 318 ancestors = cvox.DomUtil.getUniqueAncestors(prevnode, node); 319 } else { 320 ancestors = new Array(); 321 ancestors.push(node); 322 } 323 desc = cvox.DescriptionUtil.getDescriptionFromAncestors( 324 ancestors, true, cvox.ChromeVox.verbosity); 325 descriptions.push(desc); 326 return descriptions; 327 } 328 var originalNode = node; 329 var curSel = cvox.CursorSelection.fromNode(node); 330 if (!curSel) { 331 return descriptions; 332 } 333 node = cvox.DescriptionUtil.subWalker_.sync(curSel).start.node; 334 curSel = cvox.CursorSelection.fromNode(node); 335 if (!curSel) { 336 return descriptions; 337 } 338 while (cvox.DomUtil.isDescendantOfNode(node, originalNode)) { 339 descriptions = descriptions.concat( 340 cvox.DescriptionUtil.getFullDescriptionsFromChildren(prevnode, node)); 341 curSel = cvox.DescriptionUtil.subWalker_.next(curSel); 342 if (!curSel) { 343 break; 344 } 345 curSel = /** @type {!cvox.CursorSelection} */ (curSel); 346 prevnode = node; 347 node = curSel.start.node; 348 } 349 return descriptions; 350}; 351 352 353/** 354 * Modify the descriptions to say that it is a collection. 355 * @param {Array.<cvox.NavDescription>} descriptions The descriptions. 356 * @private 357 */ 358cvox.DescriptionUtil.insertCollectionDescription_ = function(descriptions) { 359 var annotations = cvox.DescriptionUtil.getAnnotations_(descriptions); 360 // If all of the items have the same annotation, describe it as a 361 // <annotation> collection with <n> items. Currently only enabled 362 // for links, but support should be added for any other type that 363 // makes sense. 364 if (descriptions.length >= 3 && 365 descriptions[0].context.length == 0 && 366 annotations.length == 1 && 367 annotations[0].length > 0 && 368 cvox.DescriptionUtil.isAnnotationCollection_(annotations[0])) { 369 var commonAnnotation = annotations[0]; 370 var firstContext = descriptions[0].context; 371 descriptions[0].context = ''; 372 for (var i = 0; i < descriptions.length; i++) { 373 descriptions[i].annotation = ''; 374 } 375 376 descriptions.splice(0, 0, new cvox.NavDescription({ 377 context: firstContext, 378 text: '', 379 annotation: cvox.ChromeVox.msgs.getMsg( 380 'collection', 381 [commonAnnotation, 382 cvox.ChromeVox.msgs.getNumber(descriptions.length)]) 383 })); 384 } 385}; 386 387 388/** 389 * Pulls the annotations from a description array. 390 * @param {Array.<cvox.NavDescription>} descriptions The descriptions. 391 * @return {Array.<string>} The annotations. 392 * @private 393 */ 394cvox.DescriptionUtil.getAnnotations_ = function(descriptions) { 395 var annotations = []; 396 for (var i = 0; i < descriptions.length; ++i) { 397 var description = descriptions[i]; 398 if (annotations.indexOf(description.annotation) == -1) { 399 // If we have an Internal link collection, call it Link collection. 400 // NOTE(deboer): The message comparison is a symptom of a bad design. 401 // I suspect this code belongs elsewhere but I don't know where, yet. 402 var linkMsg = cvox.ChromeVox.msgs.getMsg('tag_link'); 403 if (description.annotation.toLowerCase().indexOf(linkMsg.toLowerCase()) != 404 -1) { 405 if (annotations.indexOf(linkMsg) == -1) { 406 annotations.push(linkMsg); 407 } 408 } else { 409 annotations.push(description.annotation); 410 } 411 } 412 } 413 return annotations; 414}; 415 416 417/** 418 * Returns true if this annotation should be grouped as a collection, 419 * meaning that instead of repeating the annotation for each item, we 420 * just announce <annotation> collection with <n> items at the front. 421 * 422 * Currently enabled for links, but could be extended to support other 423 * roles that make sense. 424 * 425 * @param {string} annotation The annotation text. 426 * @return {boolean} If this annotation should be a collection. 427 * @private 428 */ 429cvox.DescriptionUtil.isAnnotationCollection_ = function(annotation) { 430 return (annotation == cvox.ChromeVox.msgs.getMsg('tag_link')); 431}; 432 433/** 434 * Determines whether to describe the exit of an ancestor chain. 435 * @param {Array.<Node>} ancestors The ancestors exited during navigation. 436 * @return {boolean} The result. 437 * @private 438 */ 439cvox.DescriptionUtil.shouldDescribeExit_ = function(ancestors) { 440 return ancestors.some(function(node) { 441 switch (node.tagName) { 442 case 'TABLE': 443 case 'MATH': 444 return true; 445 } 446 return cvox.AriaUtil.isLandmark(node); 447 }); 448}; 449 450 451// TODO(sorge): Bad naming...this thing returns *multiple* descriptions. 452/** 453 * Generates a description for a math node. 454 * @param {!Node} node The given node. 455 * @return {!Array.<cvox.NavDescription>} A list of Navigation descriptions. 456 */ 457cvox.DescriptionUtil.getMathDescription = function(node) { 458 // TODO (sorge) This function should evantually be removed. Descriptions 459 // should come directly from the speech rule engine, taking information on 460 // verbosity etc. into account. 461 var speechEngine = cvox.SpeechRuleEngine.getInstance(); 462 var traverse = cvox.TraverseMath.getInstance(); 463 speechEngine.parameterize(cvox.MathmlStore.getInstance()); 464 traverse.initialize(node); 465 var ret = speechEngine.evaluateNode(traverse.activeNode); 466 if (ret == []) { 467 return [new cvox.NavDescription({'text': 'empty math'})]; 468 } 469 if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) { 470 ret[ret.length - 1].annotation = 'math'; 471 } 472 ret[0].pushEarcon(cvox.AbstractEarcons.SPECIAL_CONTENT); 473 return ret; 474}; 475