navigation_history.js revision cedac228d2dd51db4b79ea1e72c7f249408ee061
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.
4
5/**
6 * @fileoverview Navigation history tracks recently visited nodes. The
7 * state of this class (the node history), is used to ensure the user is
8 * navigating to and from valid nodes.
9 * NOTE: The term "valid node" is simply a heuristic, defined in isValidNode_.
10 *
11 */
12
13
14goog.provide('cvox.NavigationHistory');
15
16goog.require('cvox.DomUtil');
17
18
19/**
20 * @constructor
21 */
22cvox.NavigationHistory = function() {
23  this.reset_();
24};
25
26
27/**
28 * The maximum length of history tracked for recently visited nodes.
29 * @const
30 * @type {number}
31 * @private
32 */
33cvox.NavigationHistory.MAX_HISTORY_LEN_ = 30;
34
35
36/**
37 * Resets the navigation history.
38 * @private
39 */
40cvox.NavigationHistory.prototype.reset_ = function() {
41  var startNode = document.body;
42
43  /**
44   * An array of nodes ordered from newest to oldest in the history.
45   * The most recent nodes are at the start of the array.
46   * @type {Array.<Node>}
47   * @private
48   */
49  this.history_ = [startNode];
50
51  /**
52   * A flag to keep track of whether the last node added to the history was
53   * valid or not. If false, something strange might be going on, and we
54   * can react to this in the code.
55   * @type {boolean}
56   * @private
57   */
58  this.arrivedValid_ = true;
59
60};
61
62
63/**
64 * Update the navigation history with the current element.
65 * The most recent elements are at the start of the array.
66 * @param {Node} newNode The new node to update the history with.
67 */
68cvox.NavigationHistory.prototype.update = function(newNode) {
69  var previousNode = this.history_[0];
70
71  // Avoid pushing consecutive duplicate elements.
72  if (newNode && newNode != previousNode) {
73    this.history_.unshift(newNode);
74  }
75
76  // If list is too long, pop the last (oldest) item.
77  if (this.history_.length >
78      cvox.NavigationHistory.MAX_HISTORY_LEN_) {
79    this.history_.pop();
80  }
81
82  // Check if the node is valid upon arrival. If not, set a flag because
83  // something fishy is probably going on.
84  this.arrivedValid_ = this.isValidNode_(newNode);
85};
86
87
88/**
89 * Routinely clean out history and determine if the given node has become
90 * invalid since we arrived there (during the update call). If the node
91 * was already invalid, we will return false.
92 * @param {Node} node The node to check for validity change.
93 * @return {boolean} True if node changed state to invalid.
94 */
95cvox.NavigationHistory.prototype.becomeInvalid = function(node) {
96  // Remove any invalid nodes from history_.
97  this.clean_();
98
99  // If node was somehow already invalid on arrival, the page was probably
100  // changing very quickly. Be defensive here and allow the default
101  // navigation action by returning true.
102  if (!this.arrivedValid_) {
103    this.arrivedValid_ = true; // Reset flag.
104    return false;
105  }
106
107  // Run the validation method on the given node.
108  return !this.isValidNode_(node);
109};
110
111
112/**
113 * Determine a valid reversion for the current navigation track. A reversion
114 * provides both a current node to sync to and a previous node as context.
115 * @param {function(Node)=} opt_predicate A function that takes in a node and
116 *     returns true if it is a valid recovery candidate. Nodes that do not
117 *     match the predicate are removed as we search for a match. If no
118 *     predicate is provided, return the two most recent nodes.
119 * @return {{current: ?Node, previous: ?Node}}
120 *     The two nodes to override default navigation behavior with. Returning
121 *     null or undefined means the history is empty.
122 */
123cvox.NavigationHistory.prototype.revert = function(opt_predicate) {
124  // If the currently active element is valid, it is probably the best
125  // recovery target. Add it to the history before computing the reversion.
126  var active = document.activeElement;
127  if (active != document.body && this.isValidNode_(active)) {
128    this.update(active);
129  }
130
131  // Remove the most-recent-nodes that do not match the predicate.
132  if (opt_predicate) {
133    while (this.history_.length > 0) {
134      var node = this.history_[0];
135      if (opt_predicate(node)) {
136        break;
137      }
138      this.history_.shift();
139    }
140  }
141
142  // The reversion is just the first two nodes in the history.
143  return {current: this.history_[0], previous: this.history_[1]};
144};
145
146
147/**
148 * Remove any and all nodes from history_ that are no longer valid.
149 * @return {boolean} True if any changes were made to the history.
150 * @private
151 */
152cvox.NavigationHistory.prototype.clean_ = function() {
153  var changed = false;
154  for (var i = this.history_.length - 1; i >= 0; i--) {
155    var valid = this.isValidNode_(this.history_[i]);
156    if (!valid) {
157      this.history_.splice(i, 1);
158      changed = true;
159    }
160  }
161  return changed;
162};
163
164
165/**
166 * Determine if the given node is valid based on a heuristic.
167 * A valid node must be attached to the DOM and visible.
168 * @param {Node} node The node to validate.
169 * @return {boolean} True if node is valid.
170 * @private
171 */
172cvox.NavigationHistory.prototype.isValidNode_ = function(node) {
173  // Confirm that the element is in the DOM.
174  if (!cvox.DomUtil.isAttachedToDocument(node)) {
175    return false;
176  }
177
178  // TODO (adu): In the future we may change this to just let users know the
179  // node is invisible instead of restoring focus.
180  if (!cvox.DomUtil.isVisible(node)) {
181    return false;
182  }
183
184  return true;
185};
186