1 // Copyright 2013 Google Inc. All Rights Reserved.
4 * @fileoverview Uses ChromeVox API to enhance the search experience.
5 * @author peterxiao@google.com (Peter Xiao)
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');
19 cvox.Search = function() {
23 * Selectors to match results.
24 * @type {Object.<string, string>}
26 cvox.Search.selectors = {};
29 * Selectors for web results.
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'
42 * Selectors for image results.
44 cvox.Search.imageSelectors = {
45 IMAGE_CATEGORIES_SELECT: '#ifbc .rg_fbl',
46 IMAGE_RESULT_SELECT: '#rg_s .rg_di'
50 * Index of the currently synced result.
56 * Array of the search results.
57 * @type {Array.<Element>}
59 cvox.Search.results = [];
62 * Array of the navigation panes.
63 * @type {Array.<Element>}
65 cvox.Search.panes = [];
68 * Index of the currently synced pane.
71 cvox.Search.paneIndex;
74 * If currently synced item is a pane.
76 cvox.Search.isPane = false;
79 * Class of a selected pane.
81 cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel';
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;
97 * Sync the search result index to ChromeVox.
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);
107 cvox.Search.speakSync_();
112 * Sync the current pane index to ChromeVox.
114 cvox.Search.syncPaneToIndex = function() {
115 var pane = cvox.Search.panes[cvox.Search.paneIndex];
116 var anchor = pane.querySelector('a');
118 cvox.ChromeVox.syncToNode(anchor, true);
120 cvox.ChromeVox.syncToNode(pane, true);
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.
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)) {
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.
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);
158 * Navigate to the next / previous page.
159 * @param {boolean} next True for the next page, false for the previous.
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;
170 var prop = { endCallback: navToUrl };
172 var pageNumber = cvox.Search.getPageNumber(url);
173 if (!isNaN(pageNumber)) {
174 cvox.ChromeVox.tts.speak('Page ' + pageNumber, 0, prop);
176 cvox.ChromeVox.tts.speak('Unknown page.', 0, prop);
182 * Navigates to the currently synced pane.
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.');
190 var anchor = pane.querySelector('a');
191 cvox.ChromeVox.tts.speak(anchor.textContent);
192 var url = cvox.SearchUtil.extractURL(pane);
194 window.location = url;
199 * Follow the link to the current result.
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);
206 window.location = url;
211 * Handle the keyboard.
212 * @param {Event} evt Keydown event.
213 * @return {boolean} True if key was handled, false otherwise.
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];
221 /* TODO(peterxiao): Add cvox api call to determine cvox key. */
222 if (evt.shiftKey || evt.altKey || evt.ctrlKey) {
226 /* Do not handle if search input has focus, or if the search widget
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');
239 cvox.Search.syncToIndex();
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');
248 cvox.Search.syncToIndex();
251 case cvox.SearchConstants.KeyCode.PAGE_UP:
252 cvox.Search.navigatePage(false);
255 case cvox.SearchConstants.KeyCode.PAGE_DOWN:
256 cvox.Search.navigatePage(true);
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();
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();
271 case cvox.SearchConstants.KeyCode.ENTER:
272 if (cvox.Search.isPane) {
273 cvox.Search.goToPane();
275 cvox.Search.goToResult();
282 evt.preventDefault();
283 evt.stopPropagation();
290 * Adds the elements that match the selector to results.
291 * @param {string} selector Selector of element to add.
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));
305 * Populates the panes array.
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));
317 * Populates the results with results.
319 cvox.Search.populateResults = function() {
320 for (var prop in cvox.Search.selectors) {
321 cvox.Search.addToResultsBySelector(cvox.Search.selectors[prop]);
326 * Populates the results with ad results.
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.
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();
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.
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) {
367 * Get the ancestor of node that is a result.
368 * @param {Node} node Node.
369 * @return {Node} Result ancestor.
371 cvox.Search.getAncestorResult = function(node) {
374 for (var prop in cvox.Search.selectors) {
375 var selector = cvox.Search.selectors[prop];
376 if (curr.webkitMatchesSelector && curr.webkitMatchesSelector(selector)) {
380 curr = curr.parentNode;
386 * Sync to the correct initial node.
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;
396 if (cvox.Search.results.length > 0) {
397 cvox.Search.syncToIndex();
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];
417 var selectedHTML = selected.innerHTML;
418 switch (selectedHTML) {
421 cvox.Search.selectors = cvox.Search.webSelectors;
424 cvox.Search.selectors = cvox.Search.imageSelectors;
425 cvox.Search.observeMutation();
431 cvox.Search.populateResults();
432 cvox.Search.populatePanes();
433 cvox.Search.paneIndex = cvox.Search.getSelectedPaneIndex();
435 cvox.Search.initialSync();