Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / chromevox / extensions / searchvox / search.js
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', cvox.QueueMode.QUEUE, prop);
106   } else if (cvox.Search.index === cvox.Search.results.length - 1) {
107     cvox.ChromeVox.tts.speak('Last result', cvox.QueueMode.QUEUE, 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, cvox.QueueMode.FLUSH,
177                                prop);
178     } else {
179       cvox.ChromeVox.tts.speak('Unknown page.', cvox.QueueMode.FLUSH, prop);
180     }
181   }
182 };
183
184 /**
185  * Navigates to the currently synced pane.
186  */
187 cvox.Search.goToPane = function() {
188   var pane = cvox.Search.panes[cvox.Search.paneIndex];
189   if (pane.className === cvox.Search.SELECTED_PANE_CLASS) {
190     cvox.ChromeVox.tts.speak('You are already on that page.',
191                              cvox.QueueMode.QUEUE);
192     return;
193   }
194   var anchor = pane.querySelector('a');
195   cvox.ChromeVox.tts.speak(anchor.textContent, cvox.QueueMode.QUEUE);
196   var url = cvox.SearchUtil.extractURL(pane);
197   if (url) {
198     window.location = url;
199   }
200 };
201
202 /**
203  * Follow the link to the current result.
204  */
205 cvox.Search.goToResult = function() {
206   var result = cvox.Search.results[cvox.Search.index];
207   var resultType = cvox.Search.getResultType(result);
208   var url = resultType.getURL(result);
209   if (url) {
210     window.location = url;
211   }
212 };
213
214 /**
215  * Handle the keyboard.
216  * @param {Event} evt Keydown event.
217  * @return {boolean} True if key was handled, false otherwise.
218  */
219 cvox.Search.keyhandler = function(evt) {
220   var SEARCH_INPUT_ID = 'gbqfq';
221   var searchInput = document.getElementById(SEARCH_INPUT_ID);
222   var result = cvox.Search.results[cvox.Search.index];
223   var ret = false;
224
225   /* TODO(peterxiao): Add cvox api call to determine cvox key. */
226   if (evt.shiftKey || evt.altKey || evt.ctrlKey) {
227     return false;
228   }
229
230   /* Do not handle if search input has focus, or if the search widget
231    * has focus.
232    */
233   if (document.activeElement !== searchInput &&
234       !cvox.SearchUtil.isSearchWidgetActive()) {
235     switch (evt.keyCode) {
236     case cvox.SearchConstants.KeyCode.UP:
237       /* Add results.length because JS Modulo is silly. */
238       cvox.Search.index = cvox.SearchUtil.subOneWrap(cvox.Search.index,
239         cvox.Search.results.length);
240       if (cvox.Search.index === cvox.Search.results.length - 1) {
241         cvox.ChromeVox.earcons.playEarconByName('WRAP');
242       }
243       cvox.Search.syncToIndex();
244       break;
245
246     case cvox.SearchConstants.KeyCode.DOWN:
247       cvox.Search.index = cvox.SearchUtil.addOneWrap(cvox.Search.index,
248         cvox.Search.results.length);
249       if (cvox.Search.index === 0) {
250         cvox.ChromeVox.earcons.playEarconByName('WRAP');
251       }
252       cvox.Search.syncToIndex();
253       break;
254
255     case cvox.SearchConstants.KeyCode.PAGE_UP:
256       cvox.Search.navigatePage(false);
257       break;
258
259     case cvox.SearchConstants.KeyCode.PAGE_DOWN:
260       cvox.Search.navigatePage(true);
261       break;
262
263     case cvox.SearchConstants.KeyCode.LEFT:
264       cvox.Search.paneIndex = cvox.SearchUtil.subOneWrap(cvox.Search.paneIndex,
265         cvox.Search.panes.length);
266       cvox.Search.syncPaneToIndex();
267       break;
268
269     case cvox.SearchConstants.KeyCode.RIGHT:
270       cvox.Search.paneIndex = cvox.SearchUtil.addOneWrap(cvox.Search.paneIndex,
271         cvox.Search.panes.length);
272       cvox.Search.syncPaneToIndex();
273       break;
274
275     case cvox.SearchConstants.KeyCode.ENTER:
276       if (cvox.Search.isPane) {
277         cvox.Search.goToPane();
278       } else {
279         cvox.Search.goToResult();
280       }
281       break;
282
283     default:
284       return false;
285     }
286     evt.preventDefault();
287     evt.stopPropagation();
288     return true;
289   }
290   return false;
291 };
292
293 /**
294  * Adds the elements that match the selector to results.
295  * @param {string} selector Selector of element to add.
296  */
297 cvox.Search.addToResultsBySelector = function(selector) {
298   var nodes = document.querySelectorAll(selector);
299   for (var i = 0; i < nodes.length; i++) {
300     var node = nodes.item(i);
301     /* Do not add if empty. */
302     if (node.innerHTML !== '') {
303       cvox.Search.results.push(nodes.item(i));
304     }
305   }
306 };
307
308 /**
309  * Populates the panes array.
310  */
311 cvox.Search.populatePanes = function() {
312   cvox.Search.panes = [];
313   var PANE_SELECT = '.hdtb_mitem';
314   var paneElems = document.querySelectorAll(PANE_SELECT);
315   for (var i = 0; i < paneElems.length; i++) {
316     cvox.Search.panes.push(paneElems.item(i));
317   }
318 };
319
320 /**
321  * Populates the results with results.
322  */
323 cvox.Search.populateResults = function() {
324   for (var prop in cvox.Search.selectors) {
325     cvox.Search.addToResultsBySelector(cvox.Search.selectors[prop]);
326   }
327 };
328
329 /**
330  * Populates the results with ad results.
331  */
332 cvox.Search.populateAdResults = function() {
333   cvox.Search.results = [];
334   var ADS_SELECT = '.ads-ad';
335   cvox.Search.addToResultsBySelector(ADS_SELECT);
336 };
337
338 /**
339  * Observes mutations and updates results accordingly.
340  */
341 cvox.Search.observeMutation = function() {
342   var SEARCH_AREA_SELECT = '#rg_s';
343   var target = document.querySelector(SEARCH_AREA_SELECT);
344
345   var observer = new MutationObserver(function(mutations) {
346     cvox.Search.results = [];
347     cvox.Search.populateResults();
348   });
349
350   var config =
351       /** @type MutationObserverInit */
352       ({ attributes: true, childList: true, characterData: true });
353   observer.observe(target, config);
354 };
355
356 /**
357  * Get the current selected pane's index.
358  * @return {number} Index of selected pane.
359  */
360 cvox.Search.getSelectedPaneIndex = function() {
361   var panes = cvox.Search.panes;
362   for (var i = 0; i < panes.length; i++) {
363     if (panes[i].className === cvox.Search.SELECTED_PANE_CLASS) {
364       return i;
365     }
366   }
367   return 0;
368 };
369
370 /**
371  * Get the ancestor of node that is a result.
372  * @param {Node} node Node.
373  * @return {Node} Result ancestor.
374  */
375 cvox.Search.getAncestorResult = function(node) {
376   var curr = node;
377   while (curr) {
378     for (var prop in cvox.Search.selectors) {
379       var selector = cvox.Search.selectors[prop];
380       if (curr.webkitMatchesSelector && curr.webkitMatchesSelector(selector)) {
381         return curr;
382       }
383     }
384     curr = curr.parentNode;
385   }
386   return null;
387 };
388
389 /**
390  * Sync to the correct initial node.
391  */
392 cvox.Search.initialSync = function() {
393   var currNode = cvox.ChromeVox.navigationManager.getCurrentNode();
394   var result = cvox.Search.getAncestorResult(currNode);
395   cvox.Search.index = cvox.Search.results.indexOf(result);
396   if (cvox.Search.index === -1) {
397     cvox.Search.index = 0;
398   }
399
400   if (cvox.Search.results.length > 0) {
401     cvox.Search.syncToIndex();
402   }
403 };
404
405 /**
406  * Initialize Search.
407  */
408 cvox.Search.init = function() {
409   cvox.Search.index = 0;
410
411   /* Flush out anything that may have been speaking. */
412   cvox.ChromeVox.tts.stop();
413
414   /* Determine the type of search. */
415   var SELECTED_CLASS = 'hdtb_msel';
416   var selected = document.getElementsByClassName(SELECTED_CLASS)[0];
417   if (!selected) {
418     return;
419   }
420
421   var selectedHTML = selected.innerHTML;
422   switch (selectedHTML) {
423   case 'Web':
424   case 'News':
425     cvox.Search.selectors = cvox.Search.webSelectors;
426     break;
427   case 'Images':
428     cvox.Search.selectors = cvox.Search.imageSelectors;
429     cvox.Search.observeMutation();
430     break;
431   default:
432     return;
433   }
434
435   cvox.Search.populateResults();
436   cvox.Search.populatePanes();
437   cvox.Search.paneIndex = cvox.Search.getSelectedPaneIndex();
438
439   cvox.Search.initialSync();
440
441 };