Home | History | Annotate | Download | only in searchvox
      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 
     10 goog.provide('cvox.Search');
     11 
     12 goog.require('cvox.ChromeVox');
     13 goog.require('cvox.SearchConstants');
     14 goog.require('cvox.SearchResults');
     15 goog.require('cvox.SearchUtil');
     16 goog.require('cvox.UnknownResult');
     17 
     18 /**
     19  * @constructor
     20  */
     21 cvox.Search = function() {
     22 };
     23 
     24 /**
     25  * Selectors to match results.
     26  * @type {Object.<string, string>}
     27  */
     28 cvox.Search.selectors = {};
     29 
     30 /**
     31  * Selectors for web results.
     32  */
     33 cvox.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  */
     46 cvox.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  */
     55 cvox.Search.index;
     56 
     57 /**
     58  * Array of the search results.
     59  * @type {Array.<Element>}
     60  */
     61 cvox.Search.results = [];
     62 
     63 /**
     64  * Array of the navigation panes.
     65  * @type {Array.<Element>}
     66  */
     67 cvox.Search.panes = [];
     68 
     69 /**
     70  * Index of the currently synced pane.
     71  * @type {number}
     72  */
     73 cvox.Search.paneIndex;
     74 
     75 /**
     76  * If currently synced item is a pane.
     77  */
     78 cvox.Search.isPane = false;
     79 
     80 /**
     81  * Class of a selected pane.
     82  */
     83 cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel';
     84 
     85 
     86 /**
     87  * Speak and sync.
     88  * @private
     89  */
     90 cvox.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  */
    101 cvox.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  */
    116 cvox.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  */
    132 cvox.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  */
    147 cvox.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  */
    163 cvox.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  */
    186 cvox.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  */
    203 cvox.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  */
    217 cvox.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  */
    295 cvox.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  */
    309 cvox.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  */
    321 cvox.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  */
    330 cvox.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  */
    339 cvox.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  */
    358 cvox.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  */
    373 cvox.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  */
    390 cvox.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  */
    406 cvox.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