blob: 034be6a39ef6f2d3ed5dbf46c0557f47b0e03667 [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 Keeps track of live regions on the page and speaks updates
* when they change.
*
*/
goog.provide('cvox.LiveRegionsDeprecated');
goog.require('cvox.AriaUtil');
goog.require('cvox.ChromeVox');
goog.require('cvox.DescriptionUtil');
goog.require('cvox.DomUtil');
goog.require('cvox.NavDescription');
/**
* @constructor
*/
cvox.LiveRegionsDeprecated = function() {
};
/**
* An array of all of the elements on the page that are live regions.
* @type {Array.<Element>}
*/
cvox.LiveRegionsDeprecated.trackedRegions = [];
/**
* A parallel array to trackedRegions that stores the previous value of
* each live region, represented as an array of NavDescriptions.
* @type {Array.<Array.<cvox.NavDescription> >}
*/
cvox.LiveRegionsDeprecated.previousRegionValue = [];
/**
* @type {Date}
*/
cvox.LiveRegionsDeprecated.pageLoadTime = null;
/**
* Time in milliseconds after initial page load to ignore live region
* updates, to avoid announcing regions as they're initially created.
* The exception is alerts, they're announced when a page is loaded.
* @type {number}
* @const
*/
cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS = 2000;
/**
* @param {Date} pageLoadTime The time the page was loaded. Live region
* updates within the first INITIAL_SILENCE_MS milliseconds are ignored.
* @param {number} queueMode Interrupt or flush. Polite live region
* changes always queue.
* @param {boolean} disableSpeak true if change announcement should be disabled.
* @return {boolean} true if any regions announced.
*/
cvox.LiveRegionsDeprecated.init = function(pageLoadTime, queueMode, disableSpeak) {
if (queueMode == undefined) {
queueMode = cvox.AbstractTts.QUEUE_MODE_FLUSH;
}
cvox.LiveRegionsDeprecated.pageLoadTime = pageLoadTime;
var anyRegionsAnnounced = false;
var regions = cvox.AriaUtil.getLiveRegions(document.body);
for (var i = 0; i < regions.length; i++) {
if (cvox.LiveRegionsDeprecated.updateLiveRegion(regions[i], queueMode,
disableSpeak)) {
anyRegionsAnnounced = true;
queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
}
}
return anyRegionsAnnounced;
};
/**
* Speak relevant changes to a live region.
*
* @param {Node} region The live region node that changed.
* @param {number} queueMode Interrupt or queue. Polite live region
* changes always queue.
* @param {boolean} disableSpeak true if change announcement should be disabled.
* @return {boolean} true if the region announced a change.
*/
cvox.LiveRegionsDeprecated.updateLiveRegion = function(region, queueMode, disableSpeak) {
if (cvox.AriaUtil.getAriaBusy(region)) {
return false;
}
// Make sure it's visible.
if (!cvox.DomUtil.isVisible(region)) {
return false;
}
// Retrieve the previous value of this region if we've tracked it
// before, otherwise start tracking it.
var regionIndex = cvox.LiveRegionsDeprecated.trackedRegions.indexOf(region);
var previousValue;
if (regionIndex >= 0) {
previousValue = cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex];
} else {
regionIndex = cvox.LiveRegionsDeprecated.trackedRegions.length;
previousValue = [];
cvox.LiveRegionsDeprecated.trackedRegions.push(region);
cvox.LiveRegionsDeprecated.previousRegionValue.push([]);
}
// Get the new value.
var currentValue = cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue(region);
// If the page just loaded and this is any region type other than 'alert',
// keep track of the new value but don't announce anything. Alerts are
// the exception, they're announced on page load.
var deltaTime = new Date() - cvox.LiveRegionsDeprecated.pageLoadTime;
if (cvox.AriaUtil.getRoleAttribute(region) != 'alert' &&
deltaTime < cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS) {
cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
return false;
}
// Don't announce alerts on page load if their text and values consist of
// just whitespace.
if (cvox.AriaUtil.getRoleAttribute(region) == 'alert' &&
deltaTime < cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS) {
var regionText = '';
for (var i = 0; i < currentValue.length; i++) {
regionText += currentValue[i].text;
regionText += currentValue[i].userValue;
}
if (cvox.DomUtil.collapseWhitespace(regionText) == '') {
cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
return false;
}
}
// Create maps of values in the live region for fast hash lookup.
var previousValueMap = {};
for (var i = 0; i < previousValue.length; i++) {
previousValueMap[previousValue[i].toString()] = true;
}
var currentValueMap = {};
for (i = 0; i < currentValue.length; i++) {
currentValueMap[currentValue[i].toString()] = true;
}
// Figure out the additions and removals.
var additions = [];
if (cvox.AriaUtil.getAriaRelevant(region, 'additions')) {
for (i = 0; i < currentValue.length; i++) {
if (!previousValueMap[currentValue[i].toString()]) {
additions.push(currentValue[i]);
}
}
}
var removals = [];
if (cvox.AriaUtil.getAriaRelevant(region, 'removals')) {
for (i = 0; i < previousValue.length; i++) {
if (!currentValueMap[previousValue[i].toString()]) {
removals.push(previousValue[i]);
}
}
}
// Only speak removals if they're the only change. Otherwise, when one or
// more removals and additions happen concurrently, treat it as a change
// and just speak any additions (which includes changed nodes).
var messages = [];
if (additions.length == 0 && removals.length > 0) {
messages = [new cvox.NavDescription({
context: cvox.ChromeVox.msgs.getMsg('live_regions_removed'), text: ''
})].concat(removals);
} else {
messages = additions;
}
// Store the new value of the live region.
cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
// Return if speak is disabled or there's nothing to announce.
if (disableSpeak || messages.length == 0) {
return false;
}
// Announce the changes with the appropriate politeness level.
var live = cvox.AriaUtil.getAriaLive(region);
if (live == 'polite') {
queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
}
for (i = 0; i < messages.length; i++) {
messages[i].speak(queueMode);
queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
}
return true;
};
/**
* Recursively build up the value of a live region and return it as
* an array of NavDescriptions. Each atomic portion of the region gets a
* single string, otherwise each leaf node gets its own string. When a region
* changes, the sets of strings before and after are searched to determine
* which have changed.
*
* @param {Node} node The root node.
* @return {Array.<cvox.NavDescription>} An array of NavDescriptions
* describing atomic nodes or leaf nodes in the subtree rooted
* at this node.
*/
cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue = function(node) {
if (cvox.AriaUtil.getAriaAtomic(node) ||
cvox.DomUtil.isLeafNode(node)) {
var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
[node], true, cvox.ChromeVox.verbosity);
if (!description.isEmpty()) {
return [description];
} else {
return [];
}
}
var result = [];
// Start with the description of this node.
var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
[node], false, cvox.ChromeVox.verbosity);
if (!description.isEmpty()) {
result.push(description);
}
// Recursively add descriptions of child nodes.
for (var i = 0; i < node.childNodes.length; i++) {
var child = node.childNodes[i];
if (cvox.DomUtil.isVisible(child, {checkAncestors: false}) &&
!cvox.AriaUtil.isHidden(child)) {
var recursiveArray = cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue(child);
result = result.concat(recursiveArray);
}
}
return result;
};