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.
6 * @fileoverview A utility class for building NavDescriptions from the dom.
10 goog.provide('cvox.DescriptionUtil');
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');
25 * Lists all Node tagName's who's description is derived from its subtree.
26 * @type {Object<boolean>}
28 cvox.DescriptionUtil.COLLECTION_NODE_TYPE = {
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.
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;
53 var surroundingControl = cvox.DomUtil.getSurroundingControl(control);
54 if (surroundingControl) {
55 ancestors = [surroundingControl, control];
59 var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
60 ancestors, true, cvox.VERBOSITY_VERBOSE);
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;
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);
79 var value = cvox.DomUtil.getValue(control);
80 if (value.length > 0) {
81 description.userValue = cvox.DomUtil.collapseWhitespace(value);
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.
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.
100 cvox.DescriptionUtil.getDescriptionFromAncestors = function(
101 ancestorsArray, recursive, verbosity) {
102 if (typeof(recursive) === 'undefined') {
105 var len = ancestorsArray.length;
111 var personality = null;
115 text = cvox.DomUtil.getName(ancestorsArray[len - 1], recursive);
117 userValue = cvox.DomUtil.getValue(ancestorsArray[len - 1]);
119 for (var i = len - 1; i >= 0; i--) {
120 var node = ancestorsArray[i];
122 hint = cvox.DomUtil.getHint(node);
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') {
130 var roleText = cvox.DomUtil.getRole(node, verbosity);
132 // Use the ancestor closest to the target to be the personality.
134 personality = cvox.AuralStyleUtil.getStyleForNode(node);
136 // TODO(dtseng): Is this needed?
137 if (i < len - 1 && node.hasAttribute('role')) {
138 var name = cvox.DomUtil.getName(node, false);
140 roleText = name + ' ' + roleText;
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) +
151 if (annotation.length > 0) {
153 ' ' + roleText + ' ' + cvox.DomUtil.getState(node, true);
155 annotation = roleText + ' ' + cvox.DomUtil.getState(node, true);
159 var earcon = cvox.EarconUtil.getEarcon(node);
160 if (earcon != null && earcons.indexOf(earcon) == -1) {
161 earcons.push(earcon);
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),
170 personality: personality,
171 hint: cvox.DomUtil.collapseWhitespace(hint)
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.
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
188 cvox.DescriptionUtil.getDescriptionFromNavigation =
189 function(prevNode, node, recursive, verbosity) {
190 if (!prevNode || !node) {
194 // Specialized math descriptions.
195 if (cvox.DomUtil.isMath(node) &&
196 !cvox.AriaUtil.isMath(node)) {
197 return cvox.DescriptionUtil.getMathDescription(node);
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)));
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) {
219 cvox.ChromeVox.msgs.getMsg('exited_container', [prevDesc.context]);
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.
237 cvox.DescriptionUtil.getCollectionDescription = function(prevSel, sel) {
238 var descriptions = cvox.DescriptionUtil.getRawDescriptions_(prevSel, sel);
239 cvox.DescriptionUtil.insertCollectionDescription_(descriptions);
245 * Used for getting collection descriptions.
246 * @type {!cvox.BareObjectWalker}
249 cvox.DescriptionUtil.subWalker_ = new cvox.BareObjectWalker();
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.
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 = [];
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;
269 var prevNode = prevSel.end.node;
270 var curSel = cvox.CursorSelection.fromNode(node);
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)) {
282 descriptions.concat(cvox.DescriptionUtil.getMathDescription(node));
284 var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
285 ancestors, true, cvox.ChromeVox.verbosity);
286 descriptions.push(description);
288 curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
293 curSel = /** @type {!cvox.CursorSelection} */ (curSel);
295 node = curSel.start.node;
302 * Returns the full descriptions of the child nodes that would be gotten by an
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.
308 cvox.DescriptionUtil.getFullDescriptionsFromChildren =
309 function(prevnode, node) {
310 var descriptions = [];
315 if (cvox.DomUtil.isLeafNode(node)) {
318 ancestors = cvox.DomUtil.getUniqueAncestors(prevnode, node);
320 ancestors = new Array();
321 ancestors.push(node);
323 desc = cvox.DescriptionUtil.getDescriptionFromAncestors(
324 ancestors, true, cvox.ChromeVox.verbosity);
325 descriptions.push(desc);
328 var originalNode = node;
329 var curSel = cvox.CursorSelection.fromNode(node);
333 node = cvox.DescriptionUtil.subWalker_.sync(curSel).start.node;
334 curSel = cvox.CursorSelection.fromNode(node);
338 while (cvox.DomUtil.isDescendantOfNode(node, originalNode)) {
339 descriptions = descriptions.concat(
340 cvox.DescriptionUtil.getFullDescriptionsFromChildren(prevnode, node));
341 curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
345 curSel = /** @type {!cvox.CursorSelection} */ (curSel);
347 node = curSel.start.node;
354 * Modify the descriptions to say that it is a collection.
355 * @param {Array<cvox.NavDescription>} descriptions The descriptions.
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
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 = '';
376 descriptions.splice(0, 0, new cvox.NavDescription({
377 context: firstContext,
379 annotation: cvox.ChromeVox.msgs.getMsg(
382 cvox.ChromeVox.msgs.getNumber(descriptions.length)])
389 * Pulls the annotations from a description array.
390 * @param {Array<cvox.NavDescription>} descriptions The descriptions.
391 * @return {Array<string>} The annotations.
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()) !=
405 if (annotations.indexOf(linkMsg) == -1) {
406 annotations.push(linkMsg);
409 annotations.push(description.annotation);
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.
422 * Currently enabled for links, but could be extended to support other
423 * roles that make sense.
425 * @param {string} annotation The annotation text.
426 * @return {boolean} If this annotation should be a collection.
429 cvox.DescriptionUtil.isAnnotationCollection_ = function(annotation) {
430 return (annotation == cvox.ChromeVox.msgs.getMsg('tag_link'));
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.
439 cvox.DescriptionUtil.shouldDescribeExit_ = function(ancestors) {
440 return ancestors.some(function(node) {
441 switch (node.tagName) {
446 return cvox.AriaUtil.isLandmark(node);
451 // TODO(sorge): Bad naming...this thing returns *multiple* descriptions.
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.
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);
467 return [new cvox.NavDescription({'text': 'empty math'})];
469 if (cvox.ChromeVox.verbosity == cvox.VERBOSITY_VERBOSE) {
470 ret[ret.length - 1].annotation = 'math';
472 ret[0].pushEarcon(cvox.Earcon.SPECIAL_CONTENT);