| OLD | NEW |
| (Empty) | |
| 1 // Copyright (c) 2011 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 A JavaScript class for walking the DOM. |
| 7 */ |
| 8 |
| 9 |
| 10 goog.provide('cvox.LinearDomWalker'); |
| 11 |
| 12 goog.require('cvox.DomUtil'); |
| 13 goog.require('cvox.XpathUtil'); |
| 14 |
| 15 /** |
| 16 * @constructor |
| 17 */ |
| 18 cvox.LinearDomWalker = function() { |
| 19 this.currentNode = null; |
| 20 this.currentAncestors = new Array(); |
| 21 this.previousNode = null; |
| 22 this.useSmartNav = true; |
| 23 }; |
| 24 |
| 25 /** |
| 26 * @type {number} |
| 27 * If a node contains more characters than this, it should not be treated |
| 28 * as a leaf node by the smart navigation algorithm. |
| 29 * |
| 30 * This number was determined by looking at the average number of |
| 31 * characters in a paragraph: |
| 32 * http://www.fullondesign.co.uk/design/usability/ |
| 33 * 285-how-many-characters-per-a-page-is-normal.htm |
| 34 * and then trying it out on a few popular websites (CNN, BBC, |
| 35 * Google Search, etc.) and making sure it made sense. |
| 36 */ |
| 37 cvox.LinearDomWalker.SMARTNAV_MAX_CHARCOUNT = 1500; |
| 38 |
| 39 /** |
| 40 * @type {string} |
| 41 * If a node contains any of these elements, it should not be treated |
| 42 * as a leaf node by the smart navigation algorithm. |
| 43 */ |
| 44 cvox.LinearDomWalker.SMARTNAV_BREAKOUT_XPATH = './/blockquote |' + |
| 45 './/button |' + |
| 46 './/code |' + |
| 47 './/form |' + |
| 48 './/frame |' + |
| 49 './/h1 |' + |
| 50 './/h2 |' + |
| 51 './/h3 |' + |
| 52 './/h4 |' + |
| 53 './/h5 |' + |
| 54 './/h6 |' + |
| 55 './/hr |' + |
| 56 './/iframe |' + |
| 57 './/input |' + |
| 58 './/object |' + |
| 59 './/ol |' + |
| 60 './/p |' + |
| 61 './/pre |' + |
| 62 './/select |' + |
| 63 './/table |' + |
| 64 './/tr |' + |
| 65 './/ul |' + |
| 66 // Aria widget roles |
| 67 './/*[@role="alert"] |' + |
| 68 './/*[@role="alertdialog"] |' + |
| 69 './/*[@role="button"] |' + |
| 70 './/*[@role="checkbox"] |' + |
| 71 './/*[@role="combobox"] |' + |
| 72 './/*[@role="dialog"] |' + |
| 73 './/*[@role="log"] |' + |
| 74 './/*[@role="marquee"] |' + |
| 75 './/*[@role="menubar"] |' + |
| 76 './/*[@role="progressbar"] |' + |
| 77 './/*[@role="radio"] |' + |
| 78 './/*[@role="radiogroup"] |' + |
| 79 './/*[@role="scrollbar"] |' + |
| 80 './/*[@role="slider"] |' + |
| 81 './/*[@role="spinbutton"] |' + |
| 82 './/*[@role="status"] |' + |
| 83 './/*[@role="tab"] |' + |
| 84 './/*[@role="tabpanel"] |' + |
| 85 './/*[@role="textbox"] |' + |
| 86 './/*[@role="toolbar"] |' + |
| 87 './/*[@role="tooltip"] |' + |
| 88 './/*[@role="treeitem"] |' + |
| 89 // Aria structure roles |
| 90 './/*[@role="article"] |' + |
| 91 './/*[@role="document"] |' + |
| 92 './/*[@role="group"] |' + |
| 93 './/*[@role="heading"] |' + |
| 94 './/*[@role="img"] |' + |
| 95 './/*[@role="list"] |' + |
| 96 './/*[@role="math"] |' + |
| 97 './/*[@role="region"] |' + |
| 98 './/*[@role="row"] |' + |
| 99 './/*[@role="separator"]'; |
| 100 |
| 101 /** |
| 102 * Gets the currentNode for the cvox.LinearDomWalker. |
| 103 * @return {Node} The the current node. |
| 104 */ |
| 105 cvox.LinearDomWalker.prototype.getCurrentNode = function() { |
| 106 return this.currentNode; |
| 107 }; |
| 108 |
| 109 /** |
| 110 * Sets the currentNode for the cvox.LinearDomWalker. |
| 111 * @param {Node} node The node that should be treated as the current node. |
| 112 */ |
| 113 cvox.LinearDomWalker.prototype.setCurrentNode = function(node) { |
| 114 this.currentNode = node; |
| 115 this.currentAncestors = new Array(); |
| 116 var ancestor = this.currentNode; |
| 117 while (ancestor) { |
| 118 this.currentAncestors.push(ancestor); |
| 119 ancestor = ancestor.parentNode; |
| 120 } |
| 121 this.currentAncestors.reverse(); |
| 122 }; |
| 123 |
| 124 /** |
| 125 * Moves to the next node. |
| 126 * |
| 127 * @return {Node} The current node. |
| 128 */ |
| 129 cvox.LinearDomWalker.prototype.next = function() { |
| 130 this.previousNode = this.currentNode; |
| 131 |
| 132 /* Make sure the handle to the current element is still valid (attached to the |
| 133 * document); if it isn't, use the cached list of ancestors to find a valid |
| 134 * node, then resume navigation from that point. |
| 135 * The current node can be invalidated by AJAX changing content. |
| 136 */ |
| 137 if (this.currentNode && |
| 138 !cvox.DomUtil.isAttachedToDocument(this.currentNode)) { |
| 139 for (var i = this.currentAncestors.length - 1, ancestor; |
| 140 ancestor = this.currentAncestors[i]; i--) { |
| 141 if (cvox.DomUtil.isAttachedToDocument(ancestor)) { |
| 142 this.setCurrentNode(ancestor); |
| 143 // Previous-Next sequence to put us back at the correct level. |
| 144 this.previous(); |
| 145 this.next(); |
| 146 break; |
| 147 } |
| 148 } |
| 149 } |
| 150 |
| 151 return this.nextContentNode(); |
| 152 }; |
| 153 |
| 154 /** |
| 155 * Moves to the previous node. |
| 156 * |
| 157 * @return {Node} The current node. |
| 158 */ |
| 159 cvox.LinearDomWalker.prototype.previous = function() { |
| 160 this.previousNode = this.currentNode; |
| 161 |
| 162 /* Make sure the handle to the current element is still valid (attached to the |
| 163 * document); if it isn't, use the cached list of ancestors to find a valid |
| 164 * node, then resume navigation from that point. |
| 165 * The current node can be invalidated by AJAX changing content. |
| 166 */ |
| 167 if (this.currentNode && |
| 168 !cvox.DomUtil.isAttachedToDocument(this.currentNode)) { |
| 169 for (var i = this.currentAncestors.length - 1, ancestor; |
| 170 ancestor = this.currentAncestors[i]; i--) { |
| 171 if (cvox.DomUtil.isAttachedToDocument(ancestor)) { |
| 172 this.setCurrentNode(ancestor); |
| 173 // Next-previous sequence to put us back at the correct level. |
| 174 this.next(); |
| 175 this.previous(); |
| 176 break; |
| 177 } |
| 178 } |
| 179 } |
| 180 |
| 181 return this.prevContentNode(); |
| 182 }; |
| 183 |
| 184 /** |
| 185 * Moves to the next node. |
| 186 * @return {Node} The current node. |
| 187 */ |
| 188 cvox.LinearDomWalker.prototype.nextNode = function() { |
| 189 if (!this.currentNode) { |
| 190 this.setCurrentNode(document.body); |
| 191 } else { |
| 192 while (this.currentNode && (!this.currentNode.nextSibling)) { |
| 193 this.setCurrentNode(this.currentNode.parentNode); |
| 194 } |
| 195 if (this.currentNode && this.currentNode.nextSibling) { |
| 196 this.setCurrentNode(this.currentNode.nextSibling); |
| 197 } |
| 198 } |
| 199 if (!this.currentNode) { |
| 200 return null; |
| 201 } |
| 202 while (!this.isLeafNode(this.currentNode)) { |
| 203 this.setCurrentNode(this.currentNode.firstChild); |
| 204 } |
| 205 return this.currentNode; |
| 206 }; |
| 207 |
| 208 /** |
| 209 * Moves to the next node that has content. |
| 210 * @return {Node} The current node. |
| 211 */ |
| 212 cvox.LinearDomWalker.prototype.nextContentNode = function() { |
| 213 this.nextNode(); |
| 214 while (this.currentNode && !cvox.DomUtil.hasContent(this.currentNode)) { |
| 215 this.nextNode(); |
| 216 } |
| 217 return this.currentNode; |
| 218 }; |
| 219 |
| 220 /** |
| 221 * Moves to the previous node. |
| 222 * @return {Node} The current node. |
| 223 */ |
| 224 cvox.LinearDomWalker.prototype.prevNode = function() { |
| 225 if (!this.currentNode) { |
| 226 this.setCurrentNode(document.body); |
| 227 } else { |
| 228 while (this.currentNode && (!this.currentNode.previousSibling)) { |
| 229 this.setCurrentNode(this.currentNode.parentNode); |
| 230 } |
| 231 if (this.currentNode && this.currentNode.previousSibling) { |
| 232 this.setCurrentNode(this.currentNode.previousSibling); |
| 233 } |
| 234 } |
| 235 if (!this.currentNode) { |
| 236 return null; |
| 237 } |
| 238 while (!this.isLeafNode(this.currentNode)) { |
| 239 this.setCurrentNode(this.currentNode.lastChild); |
| 240 } |
| 241 return this.currentNode; |
| 242 }; |
| 243 |
| 244 /** |
| 245 * Moves to the previous node that has content. |
| 246 * @return {Node} The current node. |
| 247 */ |
| 248 cvox.LinearDomWalker.prototype.prevContentNode = function() { |
| 249 this.prevNode(); |
| 250 while (this.currentNode && !cvox.DomUtil.hasContent(this.currentNode)) { |
| 251 this.prevNode(); |
| 252 } |
| 253 return this.currentNode; |
| 254 }; |
| 255 |
| 256 /** |
| 257 * Returns an array of ancestors that are unique for the current node when |
| 258 * compared to the previous node. Having such an array is useful in generating |
| 259 * the node information (identifying when interesting node boundaries have been |
| 260 * crossed, etc.). |
| 261 * |
| 262 * @return {Array.<Node>} An array of unique ancestors for the current node. |
| 263 */ |
| 264 cvox.LinearDomWalker.prototype.getUniqueAncestors = function() { |
| 265 return cvox.DomUtil.getUniqueAncestors(this.previousNode, |
| 266 this.currentNode); |
| 267 }; |
| 268 |
| 269 /** |
| 270 * Checks if Smart Nav is enabled. |
| 271 * @return {boolean} Whether Smart Nav is enabled. |
| 272 */ |
| 273 cvox.LinearDomWalker.prototype.getSmartNavEnabled = function() { |
| 274 return this.useSmartNav; |
| 275 }; |
| 276 |
| 277 /** |
| 278 * Enables/disables Smart Nav. |
| 279 * @param {boolean} enableSmartNav - Whether Smart Nav should be enabled. |
| 280 */ |
| 281 cvox.LinearDomWalker.prototype.setSmartNavEnabled = function(enableSmartNav) { |
| 282 this.useSmartNav = enableSmartNav; |
| 283 }; |
| 284 |
| 285 |
| 286 /** |
| 287 * Determines if the a node should be treated as a leaf node. |
| 288 * Based on DomUtil.isLeafNode - if SmartNav is enabled, then a few additional |
| 289 * heuristics will be applied to determine if a node can be treated as a leaf |
| 290 * node for smoother reading. |
| 291 * @param {Node} targetNode to check. |
| 292 * @return {boolean} True if targetNode can be considered a leaf node. |
| 293 */ |
| 294 cvox.LinearDomWalker.prototype.isLeafNode = function(targetNode) { |
| 295 if (cvox.DomUtil.isLeafNode(targetNode)) { |
| 296 return true; |
| 297 } |
| 298 if (!this.useSmartNav) { |
| 299 return false; |
| 300 } |
| 301 var content = cvox.DomUtil.getText(targetNode); |
| 302 if (content.length > cvox.LinearDomWalker.SMARTNAV_MAX_CHARCOUNT) { |
| 303 return false; |
| 304 } |
| 305 if (content.replace(/\s/g, '') === '') { |
| 306 // Text only contains whitespace |
| 307 return false; |
| 308 } |
| 309 var breakingNodes = cvox.XpathUtil.evalXPath( |
| 310 cvox.LinearDomWalker.SMARTNAV_BREAKOUT_XPATH, targetNode); |
| 311 for (var i = 0, node; node = breakingNodes[i]; i++) { |
| 312 if (cvox.DomUtil.hasContent(node)) { |
| 313 return false; |
| 314 } |
| 315 } |
| 316 return true; |
| 317 }; |
| OLD | NEW |