OLD | NEW |
| (Empty) |
1 // Copyright 2013 Google Inc. All Rights Reserved. | |
2 | |
3 /** | |
4 * @fileoverview Uses ChromeVox API to enhance the search experience. | |
5 * @author peterxiao@google.com (Peter Xiao) | |
6 */ | |
7 | |
8 goog.provide('cvox.Search'); | |
9 | |
10 goog.require('cvox.ChromeVox'); | |
11 goog.require('cvox.SearchConstants'); | |
12 goog.require('cvox.SearchResults'); | |
13 goog.require('cvox.SearchUtil'); | |
14 goog.require('cvox.UnknownResult'); | |
15 | |
16 /** | |
17 * @constructor | |
18 */ | |
19 cvox.Search = function() { | |
20 }; | |
21 | |
22 /** | |
23 * Selectors to match results. | |
24 * @type {Object.<string, string>} | |
25 */ | |
26 cvox.Search.selectors = {}; | |
27 | |
28 /** | |
29 * Selectors for web results. | |
30 */ | |
31 cvox.Search.webSelectors = { | |
32 /* Topstuff typically contains important messages to be added first. */ | |
33 TOPSTUFF_SELECT: '#topstuff', | |
34 SPELL_SUGG_SELECT: '.ssp', | |
35 SPELL_CORRECTION_SELECT: '.sp_cnt', | |
36 KNOW_PANEL_SELECT: '.knop', | |
37 RESULT_SELECT: 'li.g', | |
38 RELATED_SELECT: '#brs' | |
39 }; | |
40 | |
41 /** | |
42 * Selectors for image results. | |
43 */ | |
44 cvox.Search.imageSelectors = { | |
45 IMAGE_CATEGORIES_SELECT: '#ifbc .rg_fbl', | |
46 IMAGE_RESULT_SELECT: '#rg_s .rg_di' | |
47 }; | |
48 | |
49 /** | |
50 * Index of the currently synced result. | |
51 * @type {number} | |
52 */ | |
53 cvox.Search.index; | |
54 | |
55 /** | |
56 * Array of the search results. | |
57 * @type {Array.<Element>} | |
58 */ | |
59 cvox.Search.results = []; | |
60 | |
61 /** | |
62 * Array of the navigation panes. | |
63 * @type {Array.<Element>} | |
64 */ | |
65 cvox.Search.panes = []; | |
66 | |
67 /** | |
68 * Index of the currently synced pane. | |
69 * @type {number} | |
70 */ | |
71 cvox.Search.paneIndex; | |
72 | |
73 /** | |
74 * If currently synced item is a pane. | |
75 */ | |
76 cvox.Search.isPane = false; | |
77 | |
78 /** | |
79 * Class of a selected pane. | |
80 */ | |
81 cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel'; | |
82 | |
83 | |
84 /** | |
85 * Speak and sync. | |
86 * @private | |
87 */ | |
88 cvox.Search.speakSync_ = function() { | |
89 var result = cvox.Search.results[cvox.Search.index]; | |
90 var resultType = cvox.Search.getResultType(result); | |
91 var isSpoken = resultType.speak(result); | |
92 cvox.ChromeVox.syncToNode(resultType.getSyncNode(result), !isSpoken); | |
93 cvox.Search.isPane = false; | |
94 }; | |
95 | |
96 /** | |
97 * Sync the search result index to ChromeVox. | |
98 */ | |
99 cvox.Search.syncToIndex = function() { | |
100 cvox.ChromeVox.tts.stop(); | |
101 var prop = { endCallback: cvox.Search.speakSync_ }; | |
102 if (cvox.Search.index === 0) { | |
103 cvox.ChromeVox.tts.speak('First result', 1, prop); | |
104 } else if (cvox.Search.index === cvox.Search.results.length - 1) { | |
105 cvox.ChromeVox.tts.speak('Last result', 1, prop); | |
106 } else { | |
107 cvox.Search.speakSync_(); | |
108 } | |
109 }; | |
110 | |
111 /** | |
112 * Sync the current pane index to ChromeVox. | |
113 */ | |
114 cvox.Search.syncPaneToIndex = function() { | |
115 var pane = cvox.Search.panes[cvox.Search.paneIndex]; | |
116 var anchor = pane.querySelector('a'); | |
117 if (anchor) { | |
118 cvox.ChromeVox.syncToNode(anchor, true); | |
119 } else { | |
120 cvox.ChromeVox.syncToNode(pane, true); | |
121 } | |
122 cvox.Search.isPane = true; | |
123 }; | |
124 | |
125 /** | |
126 * Get the type of the result such as Knowledge Panel, Weather, etc. | |
127 * @param {Element} result Result to be classified. | |
128 * @return {cvox.AbstractResult} Type of the result. | |
129 */ | |
130 cvox.Search.getResultType = function(result) { | |
131 for (var i = 0; i < cvox.SearchResults.RESULT_TYPES.length; i++) { | |
132 var resultType = new cvox.SearchResults.RESULT_TYPES[i](); | |
133 if (resultType.isType(result)) { | |
134 return resultType; | |
135 } | |
136 } | |
137 return new cvox.UnknownResult(); | |
138 }; | |
139 | |
140 /** | |
141 * Get the page number associated with the url. | |
142 * @param {string} url Url of search page. | |
143 * @return {number} Page number. | |
144 */ | |
145 cvox.Search.getPageNumber = function(url) { | |
146 var PAGE_ANCHOR_SELECTOR = '#nav .fl'; | |
147 var pageAnchors = document.querySelectorAll(PAGE_ANCHOR_SELECTOR); | |
148 for (var i = 0; i < pageAnchors.length; i++) { | |
149 var pageAnchor = pageAnchors.item(i); | |
150 if (pageAnchor.href === url) { | |
151 return parseInt(pageAnchor.innerText, 10); | |
152 } | |
153 } | |
154 return NaN; | |
155 }; | |
156 | |
157 /** | |
158 * Navigate to the next / previous page. | |
159 * @param {boolean} next True for the next page, false for the previous. | |
160 */ | |
161 cvox.Search.navigatePage = function(next) { | |
162 /* NavEnd contains previous / next page links. */ | |
163 var NAV_END_CLASS = 'navend'; | |
164 var navEnds = document.getElementsByClassName(NAV_END_CLASS); | |
165 var navEnd = next ? navEnds[1] : navEnds[0]; | |
166 var url = cvox.SearchUtil.extractURL(navEnd); | |
167 var navToUrl = function() { | |
168 window.location = url; | |
169 }; | |
170 var prop = { endCallback: navToUrl }; | |
171 if (url) { | |
172 var pageNumber = cvox.Search.getPageNumber(url); | |
173 if (!isNaN(pageNumber)) { | |
174 cvox.ChromeVox.tts.speak('Page ' + pageNumber, 0, prop); | |
175 } else { | |
176 cvox.ChromeVox.tts.speak('Unknown page.', 0, prop); | |
177 } | |
178 } | |
179 }; | |
180 | |
181 /** | |
182 * Navigates to the currently synced pane. | |
183 */ | |
184 cvox.Search.goToPane = function() { | |
185 var pane = cvox.Search.panes[cvox.Search.paneIndex]; | |
186 if (pane.className === cvox.Search.SELECTED_PANE_CLASS) { | |
187 cvox.ChromeVox.tts.speak('You are already on that page.'); | |
188 return; | |
189 } | |
190 var anchor = pane.querySelector('a'); | |
191 cvox.ChromeVox.tts.speak(anchor.textContent); | |
192 var url = cvox.SearchUtil.extractURL(pane); | |
193 if (url) { | |
194 window.location = url; | |
195 } | |
196 }; | |
197 | |
198 /** | |
199 * Follow the link to the current result. | |
200 */ | |
201 cvox.Search.goToResult = function() { | |
202 var result = cvox.Search.results[cvox.Search.index]; | |
203 var resultType = cvox.Search.getResultType(result); | |
204 var url = resultType.getURL(result); | |
205 if (url) { | |
206 window.location = url; | |
207 } | |
208 }; | |
209 | |
210 /** | |
211 * Handle the keyboard. | |
212 * @param {Event} evt Keydown event. | |
213 * @return {boolean} True if key was handled, false otherwise. | |
214 */ | |
215 cvox.Search.keyhandler = function(evt) { | |
216 var SEARCH_INPUT_ID = 'gbqfq'; | |
217 var searchInput = document.getElementById(SEARCH_INPUT_ID); | |
218 var result = cvox.Search.results[cvox.Search.index]; | |
219 var ret = false; | |
220 | |
221 /* TODO(peterxiao): Add cvox api call to determine cvox key. */ | |
222 if (evt.shiftKey || evt.altKey || evt.ctrlKey) { | |
223 return false; | |
224 } | |
225 | |
226 /* Do not handle if search input has focus, or if the search widget | |
227 * has focus. | |
228 */ | |
229 if (document.activeElement !== searchInput && | |
230 !cvox.SearchUtil.isSearchWidgetActive()) { | |
231 switch (evt.keyCode) { | |
232 case cvox.SearchConstants.KeyCode.UP: | |
233 /* Add results.length because JS Modulo is silly. */ | |
234 cvox.Search.index = cvox.SearchUtil.subOneWrap(cvox.Search.index, | |
235 cvox.Search.results.length); | |
236 if (cvox.Search.index === cvox.Search.results.length - 1) { | |
237 cvox.ChromeVox.earcons.playEarconByName('WRAP'); | |
238 } | |
239 cvox.Search.syncToIndex(); | |
240 break; | |
241 | |
242 case cvox.SearchConstants.KeyCode.DOWN: | |
243 cvox.Search.index = cvox.SearchUtil.addOneWrap(cvox.Search.index, | |
244 cvox.Search.results.length); | |
245 if (cvox.Search.index === 0) { | |
246 cvox.ChromeVox.earcons.playEarconByName('WRAP'); | |
247 } | |
248 cvox.Search.syncToIndex(); | |
249 break; | |
250 | |
251 case cvox.SearchConstants.KeyCode.PAGE_UP: | |
252 cvox.Search.navigatePage(false); | |
253 break; | |
254 | |
255 case cvox.SearchConstants.KeyCode.PAGE_DOWN: | |
256 cvox.Search.navigatePage(true); | |
257 break; | |
258 | |
259 case cvox.SearchConstants.KeyCode.LEFT: | |
260 cvox.Search.paneIndex = cvox.SearchUtil.subOneWrap(cvox.Search.paneIndex, | |
261 cvox.Search.panes.length); | |
262 cvox.Search.syncPaneToIndex(); | |
263 break; | |
264 | |
265 case cvox.SearchConstants.KeyCode.RIGHT: | |
266 cvox.Search.paneIndex = cvox.SearchUtil.addOneWrap(cvox.Search.paneIndex, | |
267 cvox.Search.panes.length); | |
268 cvox.Search.syncPaneToIndex(); | |
269 break; | |
270 | |
271 case cvox.SearchConstants.KeyCode.ENTER: | |
272 if (cvox.Search.isPane) { | |
273 cvox.Search.goToPane(); | |
274 } else { | |
275 cvox.Search.goToResult(); | |
276 } | |
277 break; | |
278 | |
279 default: | |
280 return false; | |
281 } | |
282 evt.preventDefault(); | |
283 evt.stopPropagation(); | |
284 return true; | |
285 } | |
286 return false; | |
287 }; | |
288 | |
289 /** | |
290 * Adds the elements that match the selector to results. | |
291 * @param {string} selector Selector of element to add. | |
292 */ | |
293 cvox.Search.addToResultsBySelector = function(selector) { | |
294 var nodes = document.querySelectorAll(selector); | |
295 for (var i = 0; i < nodes.length; i++) { | |
296 var node = nodes.item(i); | |
297 /* Do not add if empty. */ | |
298 if (node.innerHTML !== '') { | |
299 cvox.Search.results.push(nodes.item(i)); | |
300 } | |
301 } | |
302 }; | |
303 | |
304 /** | |
305 * Populates the panes array. | |
306 */ | |
307 cvox.Search.populatePanes = function() { | |
308 cvox.Search.panes = []; | |
309 var PANE_SELECT = '.hdtb_mitem'; | |
310 var paneElems = document.querySelectorAll(PANE_SELECT); | |
311 for (var i = 0; i < paneElems.length; i++) { | |
312 cvox.Search.panes.push(paneElems.item(i)); | |
313 } | |
314 }; | |
315 | |
316 /** | |
317 * Populates the results with results. | |
318 */ | |
319 cvox.Search.populateResults = function() { | |
320 for (var prop in cvox.Search.selectors) { | |
321 cvox.Search.addToResultsBySelector(cvox.Search.selectors[prop]); | |
322 } | |
323 }; | |
324 | |
325 /** | |
326 * Populates the results with ad results. | |
327 */ | |
328 cvox.Search.populateAdResults = function() { | |
329 cvox.Search.results = []; | |
330 var ADS_SELECT = '.ads-ad'; | |
331 cvox.Search.addToResultsBySelector(ADS_SELECT); | |
332 }; | |
333 | |
334 /** | |
335 * Observes mutations and updates results accordingly. | |
336 */ | |
337 cvox.Search.observeMutation = function() { | |
338 var SEARCH_AREA_SELECT = '#rg_s'; | |
339 var target = document.querySelector(SEARCH_AREA_SELECT); | |
340 | |
341 var observer = new MutationObserver(function(mutations) { | |
342 cvox.Search.results = []; | |
343 cvox.Search.populateResults(); | |
344 }); | |
345 | |
346 var config = | |
347 /** @type MutationObserverInit */ | |
348 ({ attributes: true, childList: true, characterData: true }); | |
349 observer.observe(target, config); | |
350 }; | |
351 | |
352 /** | |
353 * Get the current selected pane's index. | |
354 * @return {number} Index of selected pane. | |
355 */ | |
356 cvox.Search.getSelectedPaneIndex = function() { | |
357 var panes = cvox.Search.panes; | |
358 for (var i = 0; i < panes.length; i++) { | |
359 if (panes[i].className === cvox.Search.SELECTED_PANE_CLASS) { | |
360 return i; | |
361 } | |
362 } | |
363 return 0; | |
364 }; | |
365 | |
366 /** | |
367 * Get the ancestor of node that is a result. | |
368 * @param {Node} node Node. | |
369 * @return {Node} Result ancestor. | |
370 */ | |
371 cvox.Search.getAncestorResult = function(node) { | |
372 var curr = node; | |
373 while (curr) { | |
374 for (var prop in cvox.Search.selectors) { | |
375 var selector = cvox.Search.selectors[prop]; | |
376 if (curr.webkitMatchesSelector && curr.webkitMatchesSelector(selector)) { | |
377 return curr; | |
378 } | |
379 } | |
380 curr = curr.parentNode; | |
381 } | |
382 return null; | |
383 }; | |
384 | |
385 /** | |
386 * Sync to the correct initial node. | |
387 */ | |
388 cvox.Search.initialSync = function() { | |
389 var currNode = cvox.ChromeVox.navigationManager.getCurrentNode(); | |
390 var result = cvox.Search.getAncestorResult(currNode); | |
391 cvox.Search.index = cvox.Search.results.indexOf(result); | |
392 if (cvox.Search.index === -1) { | |
393 cvox.Search.index = 0; | |
394 } | |
395 | |
396 if (cvox.Search.results.length > 0) { | |
397 cvox.Search.syncToIndex(); | |
398 } | |
399 }; | |
400 | |
401 /** | |
402 * Initialize Search. | |
403 */ | |
404 cvox.Search.init = function() { | |
405 cvox.Search.index = 0; | |
406 | |
407 /* Flush out anything that may have been speaking. */ | |
408 cvox.ChromeVox.tts.stop(); | |
409 | |
410 /* Determine the type of search. */ | |
411 var SELECTED_CLASS = 'hdtb_msel'; | |
412 var selected = document.getElementsByClassName(SELECTED_CLASS)[0]; | |
413 if (!selected) { | |
414 return; | |
415 } | |
416 | |
417 var selectedHTML = selected.innerHTML; | |
418 switch (selectedHTML) { | |
419 case 'Web': | |
420 case 'News': | |
421 cvox.Search.selectors = cvox.Search.webSelectors; | |
422 break; | |
423 case 'Images': | |
424 cvox.Search.selectors = cvox.Search.imageSelectors; | |
425 cvox.Search.observeMutation(); | |
426 break; | |
427 default: | |
428 return; | |
429 } | |
430 | |
431 cvox.Search.populateResults(); | |
432 cvox.Search.populatePanes(); | |
433 cvox.Search.paneIndex = cvox.Search.getSelectedPaneIndex(); | |
434 | |
435 cvox.Search.initialSync(); | |
436 | |
437 }; | |
OLD | NEW |