• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
8goog.provide('cvox.Search');
9
10goog.require('cvox.ChromeVox');
11goog.require('cvox.SearchConstants');
12goog.require('cvox.SearchResults');
13goog.require('cvox.SearchUtil');
14goog.require('cvox.UnknownResult');
15
16/**
17 * @constructor
18 */
19cvox.Search = function() {
20};
21
22/**
23 * Selectors to match results.
24 * @type {Object.<string, string>}
25 */
26cvox.Search.selectors = {};
27
28/**
29 * Selectors for web results.
30 */
31cvox.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 */
44cvox.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 */
53cvox.Search.index;
54
55/**
56 * Array of the search results.
57 * @type {Array.<Element>}
58 */
59cvox.Search.results = [];
60
61/**
62 * Array of the navigation panes.
63 * @type {Array.<Element>}
64 */
65cvox.Search.panes = [];
66
67/**
68 * Index of the currently synced pane.
69 * @type {number}
70 */
71cvox.Search.paneIndex;
72
73/**
74 * If currently synced item is a pane.
75 */
76cvox.Search.isPane = false;
77
78/**
79 * Class of a selected pane.
80 */
81cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel';
82
83
84/**
85 * Speak and sync.
86 * @private
87 */
88cvox.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 */
99cvox.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 */
114cvox.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 */
130cvox.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 */
145cvox.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 */
161cvox.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 */
184cvox.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 */
201cvox.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 */
215cvox.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 */
293cvox.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 */
307cvox.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 */
319cvox.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 */
328cvox.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 */
337cvox.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 */
356cvox.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 */
371cvox.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 */
388cvox.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 */
404cvox.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};
438