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
.AbstractEarcons
.SPECIAL_CONTENT
);