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