Home | History | Annotate | Download | only in searchvox
      1 // Copyright 2013 Google Inc. All Rights Reserved.
      2 
      3 /**
      4  * @fileoverview Uses ChromeVox API to enhance the search experience.
      5  * @author peterxiao (a] 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 };
    438