Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / third_party / chromevox / extensions / searchvox / search.js
blob804d51f5c8099a44b0bbc189d86c501a0713efb9
1 // Copyright 2013 Google Inc. All Rights Reserved.
3 /**
4  * @fileoverview Uses ChromeVox API to enhance the search experience.
5  * @author peterxiao@google.com (Peter Xiao)
6  */
8 goog.provide('cvox.Search');
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');
16 /**
17  * @constructor
18  */
19 cvox.Search = function() {
22 /**
23  * Selectors to match results.
24  * @type {Object.<string, string>}
25  */
26 cvox.Search.selectors = {};
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'
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'
49 /**
50  * Index of the currently synced result.
51  * @type {number}
52  */
53 cvox.Search.index;
55 /**
56  * Array of the search results.
57  * @type {Array.<Element>}
58  */
59 cvox.Search.results = [];
61 /**
62  * Array of the navigation panes.
63  * @type {Array.<Element>}
64  */
65 cvox.Search.panes = [];
67 /**
68  * Index of the currently synced pane.
69  * @type {number}
70  */
71 cvox.Search.paneIndex;
73 /**
74  * If currently synced item is a pane.
75  */
76 cvox.Search.isPane = false;
78 /**
79  * Class of a selected pane.
80  */
81 cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel';
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;
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   }
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;
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();
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;
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   }
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   }
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   }
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;
221   /* TODO(peterxiao): Add cvox api call to determine cvox key. */
222   if (evt.shiftKey || evt.altKey || evt.ctrlKey) {
223     return false;
224   }
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;
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;
251     case cvox.SearchConstants.KeyCode.PAGE_UP:
252       cvox.Search.navigatePage(false);
253       break;
255     case cvox.SearchConstants.KeyCode.PAGE_DOWN:
256       cvox.Search.navigatePage(true);
257       break;
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;
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;
271     case cvox.SearchConstants.KeyCode.ENTER:
272       if (cvox.Search.isPane) {
273         cvox.Search.goToPane();
274       } else {
275         cvox.Search.goToResult();
276       }
277       break;
279     default:
280       return false;
281     }
282     evt.preventDefault();
283     evt.stopPropagation();
284     return true;
285   }
286   return false;
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   }
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   }
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   }
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);
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);
341   var observer = new MutationObserver(function(mutations) {
342     cvox.Search.results = [];
343     cvox.Search.populateResults();
344   });
346   var config =
347       /** @type MutationObserverInit */
348       ({ attributes: true, childList: true, characterData: true });
349   observer.observe(target, config);
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;
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;
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   }
396   if (cvox.Search.results.length > 0) {
397     cvox.Search.syncToIndex();
398   }
402  * Initialize Search.
403  */
404 cvox.Search.init = function() {
405   cvox.Search.index = 0;
407   /* Flush out anything that may have been speaking. */
408   cvox.ChromeVox.tts.stop();
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   }
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   }
431   cvox.Search.populateResults();
432   cvox.Search.populatePanes();
433   cvox.Search.paneIndex = cvox.Search.getSelectedPaneIndex();
435   cvox.Search.initialSync();