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