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 Keeps track of live regions on the page and speaks updates
11 goog.provide('cvox.LiveRegionsDeprecated');
13 goog.require('cvox.AriaUtil');
14 goog.require('cvox.ChromeVox');
15 goog.require('cvox.DescriptionUtil');
16 goog.require('cvox.DomUtil');
17 goog.require('cvox.NavDescription');
22 cvox.LiveRegionsDeprecated = function() {
26 * An array of all of the elements on the page that are live regions.
27 * @type {Array.<Element>}
29 cvox.LiveRegionsDeprecated.trackedRegions = [];
32 * A parallel array to trackedRegions that stores the previous value of
33 * each live region, represented as an array of NavDescriptions.
34 * @type {Array.<Array.<cvox.NavDescription> >}
36 cvox.LiveRegionsDeprecated.previousRegionValue = [];
41 cvox.LiveRegionsDeprecated.pageLoadTime = null;
44 * Time in milliseconds after initial page load to ignore live region
45 * updates, to avoid announcing regions as they're initially created.
46 * The exception is alerts, they're announced when a page is loaded.
50 cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS = 2000;
53 * @param {Date} pageLoadTime The time the page was loaded. Live region
54 * updates within the first INITIAL_SILENCE_MS milliseconds are ignored.
55 * @param {number} queueMode Interrupt or flush. Polite live region
56 * changes always queue.
57 * @param {boolean} disableSpeak true if change announcement should be disabled.
58 * @return {boolean} true if any regions announced.
60 cvox.LiveRegionsDeprecated.init = function(pageLoadTime, queueMode, disableSpeak) {
61 if (queueMode == undefined) {
62 queueMode = cvox.AbstractTts.QUEUE_MODE_FLUSH;
65 cvox.LiveRegionsDeprecated.pageLoadTime = pageLoadTime;
67 var anyRegionsAnnounced = false;
68 var regions = cvox.AriaUtil.getLiveRegions(document.body);
69 for (var i = 0; i < regions.length; i++) {
70 if (cvox.LiveRegionsDeprecated.updateLiveRegion(regions[i], queueMode,
72 anyRegionsAnnounced = true;
73 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
77 return anyRegionsAnnounced;
81 * Speak relevant changes to a live region.
83 * @param {Node} region The live region node that changed.
84 * @param {number} queueMode Interrupt or queue. Polite live region
85 * changes always queue.
86 * @param {boolean} disableSpeak true if change announcement should be disabled.
87 * @return {boolean} true if the region announced a change.
89 cvox.LiveRegionsDeprecated.updateLiveRegion = function(region, queueMode, disableSpeak) {
90 if (cvox.AriaUtil.getAriaBusy(region)) {
94 // Make sure it's visible.
95 if (!cvox.DomUtil.isVisible(region)) {
99 // Retrieve the previous value of this region if we've tracked it
100 // before, otherwise start tracking it.
101 var regionIndex = cvox.LiveRegionsDeprecated.trackedRegions.indexOf(region);
103 if (regionIndex >= 0) {
104 previousValue = cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex];
106 regionIndex = cvox.LiveRegionsDeprecated.trackedRegions.length;
108 cvox.LiveRegionsDeprecated.trackedRegions.push(region);
109 cvox.LiveRegionsDeprecated.previousRegionValue.push([]);
112 // Get the new value.
113 var currentValue = cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue(region);
115 // If the page just loaded and this is any region type other than 'alert',
116 // keep track of the new value but don't announce anything. Alerts are
117 // the exception, they're announced on page load.
118 var deltaTime = new Date() - cvox.LiveRegionsDeprecated.pageLoadTime;
119 if (cvox.AriaUtil.getRoleAttribute(region) != 'alert' &&
120 deltaTime < cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS) {
121 cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
125 // Don't announce alerts on page load if their text and values consist of
127 if (cvox.AriaUtil.getRoleAttribute(region) == 'alert' &&
128 deltaTime < cvox.LiveRegionsDeprecated.INITIAL_SILENCE_MS) {
130 for (var i = 0; i < currentValue.length; i++) {
131 regionText += currentValue[i].text;
132 regionText += currentValue[i].userValue;
134 if (cvox.DomUtil.collapseWhitespace(regionText) == '') {
135 cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
140 // Create maps of values in the live region for fast hash lookup.
141 var previousValueMap = {};
142 for (var i = 0; i < previousValue.length; i++) {
143 previousValueMap[previousValue[i].toString()] = true;
145 var currentValueMap = {};
146 for (i = 0; i < currentValue.length; i++) {
147 currentValueMap[currentValue[i].toString()] = true;
150 // Figure out the additions and removals.
152 if (cvox.AriaUtil.getAriaRelevant(region, 'additions')) {
153 for (i = 0; i < currentValue.length; i++) {
154 if (!previousValueMap[currentValue[i].toString()]) {
155 additions.push(currentValue[i]);
160 if (cvox.AriaUtil.getAriaRelevant(region, 'removals')) {
161 for (i = 0; i < previousValue.length; i++) {
162 if (!currentValueMap[previousValue[i].toString()]) {
163 removals.push(previousValue[i]);
168 // Only speak removals if they're the only change. Otherwise, when one or
169 // more removals and additions happen concurrently, treat it as a change
170 // and just speak any additions (which includes changed nodes).
172 if (additions.length == 0 && removals.length > 0) {
173 messages = [new cvox.NavDescription({
174 context: cvox.ChromeVox.msgs.getMsg('live_regions_removed'), text: ''
175 })].concat(removals);
177 messages = additions;
180 // Store the new value of the live region.
181 cvox.LiveRegionsDeprecated.previousRegionValue[regionIndex] = currentValue;
183 // Return if speak is disabled or there's nothing to announce.
184 if (disableSpeak || messages.length == 0) {
188 // Announce the changes with the appropriate politeness level.
189 var live = cvox.AriaUtil.getAriaLive(region);
190 if (live == 'polite') {
191 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
193 for (i = 0; i < messages.length; i++) {
194 messages[i].speak(queueMode);
195 queueMode = cvox.AbstractTts.QUEUE_MODE_QUEUE;
202 * Recursively build up the value of a live region and return it as
203 * an array of NavDescriptions. Each atomic portion of the region gets a
204 * single string, otherwise each leaf node gets its own string. When a region
205 * changes, the sets of strings before and after are searched to determine
206 * which have changed.
208 * @param {Node} node The root node.
209 * @return {Array.<cvox.NavDescription>} An array of NavDescriptions
210 * describing atomic nodes or leaf nodes in the subtree rooted
213 cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue = function(node) {
214 if (cvox.AriaUtil.getAriaAtomic(node) ||
215 cvox.DomUtil.isLeafNode(node)) {
216 var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
217 [node], true, cvox.ChromeVox.verbosity);
218 if (!description.isEmpty()) {
219 return [description];
227 // Start with the description of this node.
228 var description = cvox.DescriptionUtil.getDescriptionFromAncestors(
229 [node], false, cvox.ChromeVox.verbosity);
230 if (!description.isEmpty()) {
231 result.push(description);
234 // Recursively add descriptions of child nodes.
235 for (var i = 0; i < node.childNodes.length; i++) {
236 var child = node.childNodes[i];
237 if (cvox.DomUtil.isVisible(child, {checkAncestors: false}) &&
238 !cvox.AriaUtil.isHidden(child)) {
239 var recursiveArray = cvox.LiveRegionsDeprecated.buildCurrentLiveRegionValue(child);
240 result = result.concat(recursiveArray);