Home | History | Annotate | Download | only in common
      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 
     10 goog.provide('cvox.DescriptionUtil');
     11 
     12 goog.require('cvox.AriaUtil');
     13 goog.require('cvox.AuralStyleUtil');
     14 goog.require('cvox.BareObjectWalker');
     15 goog.require('cvox.CursorSelection');
     16 goog.require('cvox.DomUtil');
     17 goog.require('cvox.EarconUtil');
     18 goog.require('cvox.MathmlStore');
     19 goog.require('cvox.NavDescription');
     20 goog.require('cvox.SpeechRuleEngine');
     21 goog.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  */
     28 cvox.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  */
     47 cvox.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  */
    100 cvox.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  */
    188 cvox.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  */
    237 cvox.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  */
    249 cvox.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  */
    259 cvox.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  */
    308 cvox.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  */
    358 cvox.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  */
    394 cvox.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  */
    429 cvox.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  */
    439 cvox.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  */
    457 cvox.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