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 JavaScript for poppup up a search widget and performing |
| 7 * search within a page. |
| 8 */ |
| 9 |
| 10 goog.provide('cvox.ChromeVoxSearch'); |
| 11 |
| 12 goog.require('cvox.AbstractEarcons'); |
| 13 goog.require('cvox.ChromeVox'); |
| 14 goog.require('cvox.SelectionUtil'); |
| 15 goog.require('cvox.XpathUtil'); |
| 16 |
| 17 /** |
| 18 * @type {Object} |
| 19 */ |
| 20 cvox.ChromeVoxSearch.txtNode = null; |
| 21 |
| 22 /** |
| 23 * @type {boolean} |
| 24 */ |
| 25 cvox.ChromeVoxSearch.active = false; |
| 26 |
| 27 /** |
| 28 * @type {Array?} |
| 29 */ |
| 30 cvox.ChromeVoxSearch.matchNodes = null; |
| 31 |
| 32 /** |
| 33 * @type {number?} |
| 34 */ |
| 35 cvox.ChromeVoxSearch.matchNodesIndex = null; |
| 36 |
| 37 /** |
| 38 * @type {boolean} |
| 39 */ |
| 40 cvox.ChromeVoxSearch.caseSensitive = false; |
| 41 |
| 42 /** |
| 43 * Initializes the search widget. |
| 44 */ |
| 45 cvox.ChromeVoxSearch.init = function() { |
| 46 cvox.ChromeVoxSearch.active = false; |
| 47 cvox.ChromeVoxSearch.caseSensitive = false; |
| 48 window.addEventListener('keypress', cvox.ChromeVoxSearch.processKeyPress, |
| 49 true); |
| 50 window.addEventListener('keydown', cvox.ChromeVoxSearch.processKeyDown, |
| 51 true); |
| 52 window.addEventListener('scroll', cvox.ChromeVoxSearch.scrollHandler, |
| 53 true); |
| 54 }; |
| 55 |
| 56 /** |
| 57 * Returns whether or not the search widget is active. |
| 58 * |
| 59 * @return {boolean} True if the search widget is active. |
| 60 */ |
| 61 cvox.ChromeVoxSearch.isActive = function() { |
| 62 return cvox.ChromeVoxSearch.active; |
| 63 }; |
| 64 |
| 65 /** |
| 66 * Displays the search widget. |
| 67 */ |
| 68 cvox.ChromeVoxSearch.show = function() { |
| 69 cvox.ChromeVoxSearch.txtNode = document.createElement('div'); |
| 70 cvox.ChromeVoxSearch.txtNode.style['display'] = 'block'; |
| 71 cvox.ChromeVoxSearch.txtNode.style['position'] = 'absolute'; |
| 72 cvox.ChromeVoxSearch.txtNode.style['left'] = '2px'; |
| 73 cvox.ChromeVoxSearch.txtNode.style['top'] = (window.scrollY + 2) + 'px'; |
| 74 cvox.ChromeVoxSearch.txtNode.style['line-height'] = '1.2em'; |
| 75 cvox.ChromeVoxSearch.txtNode.style['z-index'] = '10001'; |
| 76 cvox.ChromeVoxSearch.txtNode.style['font-size'] = '20px'; |
| 77 |
| 78 document.body.insertBefore(cvox.ChromeVoxSearch.txtNode, |
| 79 document.body.firstChild); |
| 80 cvox.ChromeVoxSearch.active = true; |
| 81 }; |
| 82 |
| 83 /** |
| 84 * Keeps the search widget at the upperleft hand corner of the screen when |
| 85 * the page scrolls. |
| 86 */ |
| 87 cvox.ChromeVoxSearch.scrollHandler = function() { |
| 88 if (!cvox.ChromeVoxSearch.active) { |
| 89 return; |
| 90 } |
| 91 cvox.ChromeVoxSearch.txtNode.style['top'] = (window.scrollY + 2) + 'px'; |
| 92 }; |
| 93 |
| 94 /** |
| 95 * Handles the keyDown event when the search widget is active. |
| 96 * |
| 97 * @param {Object} evt The keyDown event. |
| 98 * @return {boolean} Whether or not the event was handled. |
| 99 */ |
| 100 cvox.ChromeVoxSearch.processKeyDown = function(evt) { |
| 101 if (!cvox.ChromeVoxSearch.active) { |
| 102 return false; |
| 103 } |
| 104 if (evt.keyCode == 8) { // Backspace |
| 105 var searchStr = cvox.ChromeVoxSearch.txtNode.textContent; |
| 106 if (searchStr.length > 0) { |
| 107 searchStr = searchStr.substring(searchStr, searchStr.length - 1); |
| 108 cvox.ChromeVoxSearch.doSearch(searchStr, |
| 109 cvox.ChromeVoxSearch.caseSensitive); |
| 110 } |
| 111 // Don't go to the previous page! |
| 112 evt.preventDefault(); |
| 113 return true; |
| 114 } else if (evt.keyCode == 40) { // Down arrow |
| 115 cvox.ChromeVoxSearch.next(); |
| 116 return true; |
| 117 } else if (evt.keyCode == 38) { // Up arrow |
| 118 cvox.ChromeVoxSearch.prev(); |
| 119 return true; |
| 120 } else if (evt.ctrlKey && evt.keyCode == 67) { // ctrl + c |
| 121 cvox.ChromeVoxSearch.toggleCaseSensitivity(); |
| 122 return true; |
| 123 } |
| 124 return false; |
| 125 }; |
| 126 |
| 127 /** |
| 128 * Adds the letter the user typed to the search string and updates the search. |
| 129 * |
| 130 * @param {Object} evt The keyPress event. |
| 131 * @return {boolean} Whether or not the event was handled. |
| 132 */ |
| 133 cvox.ChromeVoxSearch.processKeyPress = function(evt) { |
| 134 if (!cvox.ChromeVoxSearch.active) { |
| 135 return false; |
| 136 } |
| 137 var searchStr = cvox.ChromeVoxSearch.txtNode.textContent + |
| 138 String.fromCharCode(evt.charCode); |
| 139 cvox.ChromeVoxSearch.doSearch(searchStr, |
| 140 cvox.ChromeVoxSearch.caseSensitive); |
| 141 evt.preventDefault(); |
| 142 evt.stopPropagation(); |
| 143 return true; |
| 144 }; |
| 145 |
| 146 /** |
| 147 * Toggles whether or not searches are case sensitive. |
| 148 */ |
| 149 cvox.ChromeVoxSearch.toggleCaseSensitivity = function() { |
| 150 if (cvox.ChromeVoxSearch.caseSensitive) { |
| 151 cvox.ChromeVoxSearch.caseSensitive = false; |
| 152 cvox.ChromeVox.tts.speak('Ignoring case', 0, null); |
| 153 } else { |
| 154 cvox.ChromeVoxSearch.caseSensitive = true; |
| 155 cvox.ChromeVox.tts.speak('Case sensitive', 0, null); |
| 156 } |
| 157 }; |
| 158 |
| 159 /** |
| 160 * Performs the search and highlights and speaks the first result. |
| 161 * |
| 162 * @param {String} searchStr The text to search for. |
| 163 * @param {boolean} caseSensitive Whether or not the search is case sensitive. |
| 164 */ |
| 165 cvox.ChromeVoxSearch.doSearch = function(searchStr, caseSensitive) { |
| 166 cvox.ChromeVoxSearch.txtNode.textContent = ''; |
| 167 |
| 168 cvox.ChromeVoxSearch.matchNodes = new Array(); |
| 169 var potentialMatchNodes; |
| 170 if (caseSensitive) { |
| 171 potentialMatchNodes = cvox.XpathUtil.evalXPath( |
| 172 './/text()[contains(.,"' + searchStr + '")]', |
| 173 document.body); |
| 174 } else { |
| 175 searchStr = searchStr.toLowerCase(); |
| 176 potentialMatchNodes = cvox.XpathUtil.evalXPath( |
| 177 './/text()[contains(translate(., "ABCDEFGHIJKLMNOPQRSTUVWXYZ", ' + |
| 178 '"abcdefghijklmnopqrstuvwxyz"), "' + searchStr + '")]', |
| 179 document.body); |
| 180 } |
| 181 // Only accept nodes that are considered to have content. |
| 182 for (var i = 0, node; node = potentialMatchNodes[i]; i++) { |
| 183 if (cvox.DomUtil.hasContent(node)) { |
| 184 cvox.ChromeVoxSearch.matchNodes.push(node); |
| 185 } |
| 186 } |
| 187 |
| 188 var firstNode = cvox.ChromeVoxSearch.matchNodes[0]; |
| 189 |
| 190 if (firstNode) { |
| 191 cvox.ChromeVoxSearch.matchNodesIndex = 0; |
| 192 var startIndex = 0; |
| 193 if (caseSensitive) { |
| 194 startIndex = firstNode.textContent.indexOf(searchStr); |
| 195 } else { |
| 196 startIndex = firstNode.textContent.toLowerCase().indexOf(searchStr); |
| 197 } |
| 198 cvox.SelectionUtil.selectText(firstNode, startIndex, |
| 199 startIndex + searchStr.length); |
| 200 cvox.ChromeVox.traverseContent.moveNext('sentence'); |
| 201 var sel = window.getSelection(); |
| 202 var range = sel.getRangeAt(0); |
| 203 range.setStart(firstNode, startIndex); |
| 204 sel.removeAllRanges(); |
| 205 sel.addRange(range); |
| 206 cvox.SelectionUtil.scrollToSelection(sel); |
| 207 cvox.ChromeVox.tts.speak(window.getSelection() + '', 0, null); |
| 208 } else { |
| 209 // TODO (clchen): Replace this with an error sound once we have one defined. |
| 210 cvox.ChromeVox.tts.stop(); |
| 211 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); |
| 212 } |
| 213 cvox.ChromeVoxSearch.txtNode.textContent = searchStr; |
| 214 }; |
| 215 |
| 216 /** |
| 217 * Dismisses the search widget |
| 218 */ |
| 219 cvox.ChromeVoxSearch.hide = function() { |
| 220 if (cvox.ChromeVoxSearch.active) { |
| 221 cvox.ChromeVoxSearch.txtNode.parentNode.removeChild( |
| 222 cvox.ChromeVoxSearch.txtNode); |
| 223 cvox.ChromeVoxSearch.txtNode = null; |
| 224 cvox.ChromeVoxSearch.active = false; |
| 225 } |
| 226 }; |
| 227 |
| 228 /** |
| 229 * Goes to the next matching result, highlights it, and speaks it. |
| 230 */ |
| 231 cvox.ChromeVoxSearch.next = function() { |
| 232 if (cvox.ChromeVoxSearch.matchNodes && |
| 233 (cvox.ChromeVoxSearch.matchNodes.length > 0)) { |
| 234 cvox.ChromeVoxSearch.matchNodesIndex++; |
| 235 if (cvox.ChromeVoxSearch.matchNodes.length > |
| 236 cvox.ChromeVoxSearch.matchNodesIndex) { |
| 237 var searchStr = cvox.ChromeVoxSearch.txtNode.textContent; |
| 238 var targetNode = cvox.ChromeVoxSearch.matchNodes[ |
| 239 cvox.ChromeVoxSearch.matchNodesIndex]; |
| 240 var startIndex = 0; |
| 241 if (cvox.ChromeVoxSearch.caseSensitive) { |
| 242 startIndex = targetNode.textContent.indexOf(searchStr); |
| 243 } else { |
| 244 startIndex = targetNode.textContent.toLowerCase().indexOf( |
| 245 searchStr.toLowerCase()); |
| 246 } |
| 247 cvox.SelectionUtil.selectText(targetNode, startIndex, |
| 248 startIndex + searchStr.length); |
| 249 cvox.ChromeVox.traverseContent.moveNext('sentence'); |
| 250 var sel = window.getSelection(); |
| 251 var range = sel.getRangeAt(0); |
| 252 range.setStart(targetNode, startIndex); |
| 253 sel.removeAllRanges(); |
| 254 sel.addRange(range); |
| 255 cvox.SelectionUtil.scrollToSelection(sel); |
| 256 cvox.ChromeVox.tts.speak(window.getSelection() + '', 0, null); |
| 257 } else { |
| 258 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); |
| 259 cvox.ChromeVoxSearch.matchNodesIndex = -1; |
| 260 cvox.ChromeVoxSearch.next(); |
| 261 } |
| 262 } |
| 263 }; |
| 264 |
| 265 /** |
| 266 * Goes to the previous matching result, highlights it, and speaks it. |
| 267 */ |
| 268 cvox.ChromeVoxSearch.prev = function() { |
| 269 if (cvox.ChromeVoxSearch.matchNodes && |
| 270 (cvox.ChromeVoxSearch.matchNodes.length > 0)) { |
| 271 cvox.ChromeVoxSearch.matchNodesIndex--; |
| 272 if (cvox.ChromeVoxSearch.matchNodesIndex > -1) { |
| 273 var searchStr = cvox.ChromeVoxSearch.txtNode.textContent; |
| 274 var targetNode = cvox.ChromeVoxSearch.matchNodes[ |
| 275 cvox.ChromeVoxSearch.matchNodesIndex]; |
| 276 var startIndex = 0; |
| 277 if (cvox.ChromeVoxSearch.caseSensitive) { |
| 278 startIndex = targetNode.textContent.indexOf(searchStr); |
| 279 } else { |
| 280 startIndex = targetNode.textContent.toLowerCase().indexOf( |
| 281 searchStr.toLowerCase()); |
| 282 } |
| 283 cvox.SelectionUtil.selectText(targetNode, startIndex, |
| 284 startIndex + searchStr.length); |
| 285 cvox.ChromeVox.traverseContent.moveNext('sentence'); |
| 286 var sel = window.getSelection(); |
| 287 var range = sel.getRangeAt(0); |
| 288 range.setStart(targetNode, startIndex); |
| 289 sel.removeAllRanges(); |
| 290 sel.addRange(range); |
| 291 cvox.SelectionUtil.scrollToSelection(sel); |
| 292 cvox.ChromeVox.tts.speak(window.getSelection() + '', 0, null); |
| 293 } else { |
| 294 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); |
| 295 cvox.ChromeVoxSearch.matchNodesIndex = |
| 296 cvox.ChromeVoxSearch.matchNodes.length; |
| 297 cvox.ChromeVoxSearch.prev(); |
| 298 } |
| 299 } |
| 300 }; |
OLD | NEW |