[Extensions] Make extension message bubble factory platform-abstract
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / chromevox / extensions / searchvox / search.js
blobad7feb0d57356aea168b39eecc229922c00d1521
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.
6 /**
7 * @fileoverview Uses ChromeVox API to enhance the search experience.
8 */
10 goog.provide('cvox.Search');
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');
18 /**
19 * @constructor
21 cvox.Search = function() {
24 /**
25 * Selectors to match results.
26 * @type {Object<string, string>}
28 cvox.Search.selectors = {};
30 /**
31 * Selectors for web results.
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'
43 /**
44 * Selectors for image results.
46 cvox.Search.imageSelectors = {
47 IMAGE_CATEGORIES_SELECT: '#ifbc .rg_fbl',
48 IMAGE_RESULT_SELECT: '#rg_s .rg_di'
51 /**
52 * Index of the currently synced result.
53 * @type {number}
55 cvox.Search.index;
57 /**
58 * Array of the search results.
59 * @type {Array<Element>}
61 cvox.Search.results = [];
63 /**
64 * Array of the navigation panes.
65 * @type {Array<Element>}
67 cvox.Search.panes = [];
69 /**
70 * Index of the currently synced pane.
71 * @type {number}
73 cvox.Search.paneIndex;
75 /**
76 * If currently synced item is a pane.
78 cvox.Search.isPane = false;
80 /**
81 * Class of a selected pane.
83 cvox.Search.SELECTED_PANE_CLASS = 'hdtb_mitem hdtb_msel';
86 /**
87 * Speak and sync.
88 * @private
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;
98 /**
99 * Sync the search result index to ChromeVox.
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_();
114 * Sync the current pane index to ChromeVox.
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);
124 cvox.Search.isPane = true;
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.
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;
139 return new cvox.UnknownResult();
143 * Get the page number associated with the url.
144 * @param {string} url Url of search page.
145 * @return {number} Page number.
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);
156 return NaN;
160 * Navigate to the next / previous page.
161 * @param {boolean} next True for the next page, false for the previous.
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;
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);
185 * Navigates to the currently synced pane.
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;
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;
203 * Follow the link to the current result.
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;
215 * Handle the keyboard.
216 * @param {Event} evt Keydown event.
217 * @return {boolean} True if key was handled, false otherwise.
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;
225 /* TODO(peterxiao): Add cvox api call to determine cvox key. */
226 if (evt.shiftKey || evt.altKey || evt.ctrlKey) {
227 return false;
230 /* Do not handle if search input has focus, or if the search widget
231 * has focus.
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');
243 cvox.Search.syncToIndex();
244 break;
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');
252 cvox.Search.syncToIndex();
253 break;
255 case cvox.SearchConstants.KeyCode.PAGE_UP:
256 cvox.Search.navigatePage(false);
257 break;
259 case cvox.SearchConstants.KeyCode.PAGE_DOWN:
260 cvox.Search.navigatePage(true);
261 break;
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;
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;
275 case cvox.SearchConstants.KeyCode.ENTER:
276 if (cvox.Search.isPane) {
277 cvox.Search.goToPane();
278 } else {
279 cvox.Search.goToResult();
281 break;
283 default:
284 return false;
286 evt.preventDefault();
287 evt.stopPropagation();
288 return true;
290 return false;
294 * Adds the elements that match the selector to results.
295 * @param {string} selector Selector of element to add.
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));
309 * Populates the panes array.
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));
321 * Populates the results with results.
323 cvox.Search.populateResults = function() {
324 for (var prop in cvox.Search.selectors) {
325 cvox.Search.addToResultsBySelector(cvox.Search.selectors[prop]);
330 * Populates the results with ad results.
332 cvox.Search.populateAdResults = function() {
333 cvox.Search.results = [];
334 var ADS_SELECT = '.ads-ad';
335 cvox.Search.addToResultsBySelector(ADS_SELECT);
339 * Observes mutations and updates results accordingly.
341 cvox.Search.observeMutation = function() {
342 var SEARCH_AREA_SELECT = '#rg_s';
343 var target = document.querySelector(SEARCH_AREA_SELECT);
345 var observer = new MutationObserver(function(mutations) {
346 cvox.Search.results = [];
347 cvox.Search.populateResults();
350 var config =
351 /** @type MutationObserverInit */
352 ({ attributes: true, childList: true, characterData: true });
353 observer.observe(target, config);
357 * Get the current selected pane's index.
358 * @return {number} Index of selected pane.
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;
367 return 0;
371 * Get the ancestor of node that is a result.
372 * @param {Node} node Node.
373 * @return {Node} Result ancestor.
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;
384 curr = curr.parentNode;
386 return null;
390 * Sync to the correct initial node.
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;
400 if (cvox.Search.results.length > 0) {
401 cvox.Search.syncToIndex();
406 * Initialize Search.
408 cvox.Search.init = function() {
409 cvox.Search.index = 0;
411 /* Flush out anything that may have been speaking. */
412 cvox.ChromeVox.tts.stop();
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;
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;
435 cvox.Search.populateResults();
436 cvox.Search.populatePanes();
437 cvox.Search.paneIndex = cvox.Search.getSelectedPaneIndex();
439 cvox.Search.initialSync();