blob: 36824ae7c11f57fb1cbc0a38e4f98add97c01c27 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview A utility class for building NavDescriptions from the dom.
*/
goog.provide('cvox.DescriptionUtil');
goog.require('cvox.AriaUtil');
goog.require('cvox.AuralStyleUtil');
goog.require('cvox.BareObjectWalker');
goog.require('cvox.CursorSelection');
goog.require('cvox.DomUtil');
goog.require('cvox.EarconUtil');
goog.require('cvox.NavDescription');
/**
* Lists all Node tagName's who's description is derived from its subtree.
* @type {Object<boolean>}
*/
cvox.DescriptionUtil.COLLECTION_NODE_TYPE = {
'H1': true,
'H2': true,
'H3': true,
'H4': true,
'H5': true,
'H6': true
};
/**
* Get a control's complete description in the same format as if you
* navigated to the node.
* @param {Element} control A control.
* @param {Array<Node>=} opt_changedAncestors The changed ancestors that will
* be used to determine what needs to be spoken. If this is not provided, the
* ancestors used to determine what needs to be spoken will just be the control
* itself and its surrounding control if it has one.
* @return {cvox.NavDescription} The description of the control.
*/
cvox.DescriptionUtil.getControlDescription = function(
control, opt_changedAncestors) {
var ancestors = [control];
if (opt_changedAncestors && (opt_changedAncestors.length > 0)) {
ancestors = opt_changedAncestors;
} else {
var surroundingControl = cvox.DomUtil.getSurroundingControl(control);
if (surroundingControl) {
ancestors = [surroundingControl, control];
}
}
var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
ancestors, true, cvox.VERBOSITY_VERBOSE);
// Use heuristics if the control doesn't otherwise have a name.
if (surroundingControl) {
var name = cvox.DomUtil.getName(surroundingControl);
if (name.length == 0) {
name = cvox.DomUtil.getControlLabelHeuristics(surroundingControl);
if (name.length > 0) {
description.context = name + ' ' + description.context;
}
}
} else {
var name = cvox.DomUtil.getName(control);
if (name.length == 0) {
name = cvox.DomUtil.getControlLabelHeuristics(control);
if (name.length > 0) {
description.text = cvox.DomUtil.collapseWhitespace(name);
}
}
var value = cvox.DomUtil.getValue(control);
if (value.length > 0) {
description.userValue = cvox.DomUtil.collapseWhitespace(value);
}
}
return description;
};
/**
* Returns a description of a navigation from an array of changed
* ancestor nodes. The ancestors are in order from the highest in the
* tree to the lowest, i.e. ending with the current leaf node.
*
* @param {Array<Node>} ancestorsArray An array of ancestor nodes.
* @param {boolean} recursive Whether or not the element's subtree should
* be used; true by default.
* @param {number} verbosity The verbosity setting.
* @return {cvox.NavDescription} The description of the navigation action.
*/
cvox.DescriptionUtil.getDescriptionFromAncestors = function(
ancestorsArray, recursive, verbosity) {
if (typeof(recursive) === 'undefined') {
recursive = true;
}
var len = ancestorsArray.length;
var context = '';
var text = '';
var userValue = '';
var annotation = '';
var earcons = [];
var personality = null;
var hint = '';
if (len > 0) {
text = cvox.DomUtil.getName(ancestorsArray[len - 1], recursive);
userValue = cvox.DomUtil.getValue(ancestorsArray[len - 1]);
}
for (var i = len - 1; i >= 0; i--) {
var node = ancestorsArray[i];
hint = cvox.DomUtil.getHint(node);
// Don't speak dialogs here, they're spoken when events occur.
var role = node.getAttribute ? node.getAttribute('role') : null;
if (role == 'alertdialog') {
continue;
}
var roleText = cvox.DomUtil.getRole(node, verbosity);
// Use the ancestor closest to the target to be the personality.
if (!personality) {
personality = cvox.AuralStyleUtil.getStyleForNode(node);
}
// TODO(dtseng): Is this needed?
if (i < len - 1 && node.hasAttribute('role')) {
var name = cvox.DomUtil.getName(node, false);
if (name) {
roleText = name + ' ' + roleText;
}
}
if (roleText.length > 0) {
// Since we prioritize reading of context in reading order, only populate
// it for larger ancestry changes.
if (context.length > 0 ||
(annotation.length > 0 && node.childElementCount > 1)) {
context =
roleText + ' ' + cvox.DomUtil.getState(node, false) + ' ' + context;
} else {
if (annotation.length > 0) {
annotation +=
' ' + roleText + ' ' + cvox.DomUtil.getState(node, true);
} else {
annotation = roleText + ' ' + cvox.DomUtil.getState(node, true);
}
}
}
var earcon = cvox.EarconUtil.getEarcon(node);
if (earcon != null && earcons.indexOf(earcon) == -1) {
earcons.push(earcon);
}
}
return new cvox.NavDescription({
context: cvox.DomUtil.collapseWhitespace(context),
text: cvox.DomUtil.collapseWhitespace(text),
userValue: cvox.DomUtil.collapseWhitespace(userValue),
annotation: cvox.DomUtil.collapseWhitespace(annotation),
earcons: earcons,
personality: personality,
hint: cvox.DomUtil.collapseWhitespace(hint)
});
};
/**
* Returns a description of a navigation from an array of changed
* ancestor nodes. The ancestors are in order from the highest in the
* tree to the lowest, i.e. ending with the current leaf node.
*
* @param {Node} prevNode The previous node in navigation.
* @param {Node} node The current node in navigation.
* @param {boolean} recursive Whether or not the element's subtree should
* be used; true by default.
* @param {number} verbosity The verbosity setting.
* @return {!Array<cvox.NavDescription>} The description of the navigation
* action.
*/
cvox.DescriptionUtil.getDescriptionFromNavigation = function(
prevNode, node, recursive, verbosity) {
if (!prevNode || !node) {
return [];
}
// Next, check to see if the current node is a collection type.
if (cvox.DescriptionUtil.COLLECTION_NODE_TYPE[node.tagName]) {
return cvox.DescriptionUtil.getCollectionDescription(
/** @type {!cvox.CursorSelection} */ (
cvox.CursorSelection.fromNode(prevNode)),
/** @type {!cvox.CursorSelection} */
(cvox.CursorSelection.fromNode(node)));
}
// Now, generate a description for all other elements.
var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node, true);
var desc = cvox.DescriptionUtil.getDescriptionFromAncestors(
ancestors, recursive, verbosity);
var prevAncestors = cvox.DomUtil.getUniqueAncestors(node, prevNode);
if (cvox.DescriptionUtil.shouldDescribeExit_(prevAncestors)) {
var prevDesc = cvox.DescriptionUtil.getDescriptionFromAncestors(
prevAncestors, recursive, verbosity);
if (prevDesc.context && !desc.context) {
desc.context = Msgs.getMsg('exited_container', [prevDesc.context]);
}
}
return [desc];
};
/**
* Returns an array of NavDescriptions that includes everything that would be
* spoken by an object walker while traversing from prevSel to sel.
* It also includes any necessary annotations and context about the set of
* descriptions. This function is here because most (currently all) walkers
* that iterate over non-leaf nodes need this sort of description.
* This is an awkward design, and should be changed in the future.
* @param {!cvox.CursorSelection} prevSel The previous selection.
* @param {!cvox.CursorSelection} sel The selection.
* @return {!Array<!cvox.NavDescription>} The descriptions as described above.
*/
cvox.DescriptionUtil.getCollectionDescription = function(prevSel, sel) {
var descriptions = cvox.DescriptionUtil.getRawDescriptions_(prevSel, sel);
cvox.DescriptionUtil.insertCollectionDescription_(descriptions);
return descriptions;
};
/**
* Used for getting collection descriptions.
* @type {!cvox.BareObjectWalker}
* @private
*/
cvox.DescriptionUtil.subWalker_ = new cvox.BareObjectWalker();
/**
* Returns the descriptions that would be gotten by an object walker.
* @param {!cvox.CursorSelection} prevSel The previous selection.
* @param {!cvox.CursorSelection} sel The selection.
* @return {!Array<!cvox.NavDescription>} The descriptions.
* @private
*/
cvox.DescriptionUtil.getRawDescriptions_ = function(prevSel, sel) {
// Use a object walker in non-smart mode to traverse all of the
// nodes inside the current smart node and return their annotations.
var descriptions = [];
// We want the descriptions to be in forward order whether or not the
// selection is reversed.
sel = sel.clone().setReversed(false);
var node = cvox.DescriptionUtil.subWalker_.sync(sel).start.node;
var prevNode = prevSel.end.node;
var curSel = cvox.CursorSelection.fromNode(node);
if (!curSel) {
return [];
}
while (cvox.DomUtil.isDescendantOfNode(node, sel.start.node)) {
var ancestors = cvox.DomUtil.getUniqueAncestors(prevNode, node);
var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
ancestors, true, cvox.ChromeVox.verbosity);
descriptions.push(description);
curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
if (!curSel) {
break;
}
curSel = /** @type {!cvox.CursorSelection} */ (curSel);
prevNode = node;
node = curSel.start.node;
}
return descriptions;
};
/**
* Returns the full descriptions of the child nodes that would be gotten by an
* object walker.
* @param {?Node} prevnode The previous element if there is one.
* @param {!Node} node The target element.
* @return {!Array<!cvox.NavDescription>} The descriptions.
*/
cvox.DescriptionUtil.getFullDescriptionsFromChildren = function(
prevnode, node) {
var descriptions = [];
if (!node) {
return descriptions;
}
var desc;
if (cvox.DomUtil.isLeafNode(node)) {
var ancestors;
if (prevnode) {
ancestors = cvox.DomUtil.getUniqueAncestors(prevnode, node);
} else {
ancestors = new Array();
ancestors.push(node);
}
desc = cvox.DescriptionUtil.getDescriptionFromAncestors(
ancestors, true, cvox.ChromeVox.verbosity);
descriptions.push(desc);
return descriptions;
}
var originalNode = node;
var curSel = cvox.CursorSelection.fromNode(node);
if (!curSel) {
return descriptions;
}
var newNode = cvox.DescriptionUtil.subWalker_.sync(curSel).start.node;
curSel = cvox.CursorSelection.fromNode(newNode);
if (!curSel) {
return descriptions;
}
while (newNode && cvox.DomUtil.isDescendantOfNode(newNode, originalNode)) {
descriptions = descriptions.concat(
cvox.DescriptionUtil.getFullDescriptionsFromChildren(
prevnode, newNode));
curSel = cvox.DescriptionUtil.subWalker_.next(curSel);
if (!curSel) {
break;
}
curSel = /** @type {!cvox.CursorSelection} */ (curSel);
prevnode = newNode;
newNode = curSel.start.node;
}
return descriptions;
};
/**
* Modify the descriptions to say that it is a collection.
* @param {Array<cvox.NavDescription>} descriptions The descriptions.
* @private
*/
cvox.DescriptionUtil.insertCollectionDescription_ = function(descriptions) {
var annotations = cvox.DescriptionUtil.getAnnotations_(descriptions);
// If all of the items have the same annotation, describe it as a
// <annotation> collection with <n> items. Currently only enabled
// for links, but support should be added for any other type that
// makes sense.
if (descriptions.length >= 3 && descriptions[0].context.length == 0 &&
annotations.length == 1 && annotations[0].length > 0 &&
cvox.DescriptionUtil.isAnnotationCollection_(annotations[0])) {
var commonAnnotation = annotations[0];
var firstContext = descriptions[0].context;
descriptions[0].context = '';
for (var i = 0; i < descriptions.length; i++) {
descriptions[i].annotation = '';
}
descriptions.splice(0, 0, new cvox.NavDescription({
context: firstContext,
text: '',
annotation: Msgs.getMsg(
'collection',
[commonAnnotation, Msgs.getNumber(descriptions.length)])
}));
}
};
/**
* Pulls the annotations from a description array.
* @param {Array<cvox.NavDescription>} descriptions The descriptions.
* @return {Array<string>} The annotations.
* @private
*/
cvox.DescriptionUtil.getAnnotations_ = function(descriptions) {
var annotations = [];
for (var i = 0; i < descriptions.length; ++i) {
var description = descriptions[i];
if (annotations.indexOf(description.annotation) == -1) {
// If we have an Internal link collection, call it Link collection.
// NOTE(deboer): The message comparison is a symptom of a bad design.
// I suspect this code belongs elsewhere but I don't know where, yet.
var linkMsg = Msgs.getMsg('role_link');
if (description.annotation.toLowerCase().indexOf(linkMsg.toLowerCase()) !=
-1) {
if (annotations.indexOf(linkMsg) == -1) {
annotations.push(linkMsg);
}
} else {
annotations.push(description.annotation);
}
}
}
return annotations;
};
/**
* Returns true if this annotation should be grouped as a collection,
* meaning that instead of repeating the annotation for each item, we
* just announce <annotation> collection with <n> items at the front.
*
* Currently enabled for links, but could be extended to support other
* roles that make sense.
*
* @param {string} annotation The annotation text.
* @return {boolean} If this annotation should be a collection.
* @private
*/
cvox.DescriptionUtil.isAnnotationCollection_ = function(annotation) {
return (annotation == Msgs.getMsg('role_link'));
};
/**
* Determines whether to describe the exit of an ancestor chain.
* @param {Array<Node>} ancestors The ancestors exited during navigation.
* @return {boolean} The result.
* @private
*/
cvox.DescriptionUtil.shouldDescribeExit_ = function(ancestors) {
return ancestors.some(function(node) {
switch (node.tagName) {
case 'TABLE':
case 'MATH':
return true;
}
return cvox.AriaUtil.isLandmark(node);
});
};