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.
7 * @fileoverview Uses ChromeVox API to enhance the search experience.
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');
21 cvox
.Search = function() {
25 * Selectors to match results.
26 * @type {Object<string, string>}
28 cvox
.Search
.selectors
= {};
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'
44 * Selectors for image results.
46 cvox
.Search
.imageSelectors
= {
47 IMAGE_CATEGORIES_SELECT
: '#ifbc .rg_fbl',
48 IMAGE_RESULT_SELECT
: '#rg_s .rg_di'
52 * Index of the currently synced result.
58 * Array of the search results.
59 * @type {Array<Element>}
61 cvox
.Search
.results
= [];
64 * Array of the navigation panes.
65 * @type {Array<Element>}
67 cvox
.Search
.panes
= [];
70 * Index of the currently synced pane.
73 cvox
.Search
.paneIndex
;
76 * If currently synced item is a pane.
78 cvox
.Search
.isPane
= false;
81 * Class of a selected pane.
83 cvox
.Search
.SELECTED_PANE_CLASS
= 'hdtb_mitem hdtb_msel';
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;
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
);
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');
120 cvox
.ChromeVox
.syncToNode(anchor
, true);
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
)) {
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);
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
};
174 var pageNumber
= cvox
.Search
.getPageNumber(url
);
175 if (!isNaN(pageNumber
)) {
176 cvox
.ChromeVox
.tts
.speak('Page ' + pageNumber
, cvox
.QueueMode
.FLUSH
,
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
);
194 var anchor
= pane
.querySelector('a');
195 cvox
.ChromeVox
.tts
.speak(anchor
.textContent
, cvox
.QueueMode
.QUEUE
);
196 var url
= cvox
.SearchUtil
.extractURL(pane
);
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
);
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
];
225 /* TODO(peterxiao): Add cvox api call to determine cvox key. */
226 if (evt
.shiftKey
|| evt
.altKey
|| evt
.ctrlKey
) {
230 /* Do not handle if search input has focus, or if the search widget
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();
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();
255 case cvox
.SearchConstants
.KeyCode
.PAGE_UP
:
256 cvox
.Search
.navigatePage(false);
259 case cvox
.SearchConstants
.KeyCode
.PAGE_DOWN
:
260 cvox
.Search
.navigatePage(true);
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();
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();
275 case cvox
.SearchConstants
.KeyCode
.ENTER
:
276 if (cvox
.Search
.isPane
) {
277 cvox
.Search
.goToPane();
279 cvox
.Search
.goToResult();
286 evt
.preventDefault();
287 evt
.stopPropagation();
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();
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
) {
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
) {
378 for (var prop
in cvox
.Search
.selectors
) {
379 var selector
= cvox
.Search
.selectors
[prop
];
380 if (curr
.webkitMatchesSelector
&& curr
.webkitMatchesSelector(selector
)) {
384 curr
= curr
.parentNode
;
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();
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];
421 var selectedHTML
= selected
.innerHTML
;
422 switch (selectedHTML
) {
425 cvox
.Search
.selectors
= cvox
.Search
.webSelectors
;
428 cvox
.Search
.selectors
= cvox
.Search
.imageSelectors
;
429 cvox
.Search
.observeMutation();
435 cvox
.Search
.populateResults();
436 cvox
.Search
.populatePanes();
437 cvox
.Search
.paneIndex
= cvox
.Search
.getSelectedPaneIndex();
439 cvox
.Search
.initialSync();