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 * @fileoverview JavaScript for poppup up a search widget and performing
7 * search within a page.
10 goog.provide('cvox.SearchWidget');
12 goog.require('cvox.AbstractEarcons');
13 goog.require('cvox.ApiImplementation');
14 goog.require('cvox.ChromeVox');
15 goog.require('cvox.CursorSelection');
16 goog.require('cvox.NavigationManager');
17 goog.require('cvox.Widget');
21 * Initializes the search widget.
23 * @extends {cvox.Widget}
25 cvox.SearchWidget = function() {
30 this.containerNode_ = null;
43 this.PROMPT_ = 'Search:';
49 this.caseSensitive_ = false;
55 this.hasMatch_ = false;
58 goog.inherits(cvox.SearchWidget, cvox.Widget);
59 goog.addSingletonGetter(cvox.SearchWidget);
65 cvox.SearchWidget.prototype.show = function() {
66 goog.base(this, 'show');
68 this.hasMatch_ = false;
69 cvox.ChromeVox.navigationManager.setGranularity(
70 cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false);
72 // Always start search forward.
73 cvox.ChromeVox.navigationManager.setReversed(false);
75 // During profiling, NavigationHistory was found to have a serious performance
77 this.focusRecovery_ = cvox.ChromeVox.navigationManager.getFocusRecovery();
78 cvox.ChromeVox.navigationManager.setFocusRecovery(false);
80 var containerNode = this.createContainerNode_();
81 this.containerNode_ = containerNode;
83 var overlayNode = this.createOverlayNode_();
84 containerNode.appendChild(overlayNode);
86 var promptNode = document.createElement('span');
87 promptNode.innerHTML = this.PROMPT_;
88 overlayNode.appendChild(promptNode);
90 this.txtNode_ = this.createTextAreaNode_();
92 overlayNode.appendChild(this.txtNode_);
94 document.body.appendChild(containerNode);
96 this.txtNode_.focus();
98 window.setTimeout(function() {
99 containerNode.style['opacity'] = '1.0';
107 cvox.SearchWidget.prototype.hide = function(opt_noSync) {
108 if (this.isActive()) {
109 var containerNode = this.containerNode_;
110 containerNode.style.opacity = '0.0';
111 window.setTimeout(function() {
112 document.body.removeChild(containerNode);
114 this.txtNode_ = null;
115 cvox.SearchWidget.containerNode = null;
116 cvox.ChromeVox.navigationManager.setFocusRecovery(this.focusRecovery_);
120 cvox.$m('choice_widget_exited').
122 andMessage(this.getNameMsg()).
125 if (!this.hasMatch_ || !opt_noSync) {
126 cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
129 cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind(
130 cvox.ChromeVox.navigationManager.syncAll,
131 cvox.ChromeVox.navigationManager))(true);
132 cvox.ChromeVox.navigationManager.speakDescriptionArray(
133 cvox.ChromeVox.navigationManager.getDescription(),
134 cvox.QueueMode.QUEUE,
136 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
138 // Update on Braille too.
139 // TODO: Use line granularity in search so we can simply call
140 // cvox.ChromeVox.navigationManager.getBraille().write() instead.
141 var text = this.textFromCurrentDescription_();
142 cvox.ChromeVox.braille.write(new cvox.NavBraille({
148 goog.base(this, 'hide', true);
155 cvox.SearchWidget.prototype.getNameMsg = function() {
156 return ['search_widget_intro'];
163 cvox.SearchWidget.prototype.getHelpMsg = function() {
164 return 'search_widget_intro_help';
171 cvox.SearchWidget.prototype.onKeyDown = function(evt) {
172 if (!this.isActive()) {
175 var searchStr = this.txtNode_.value;
176 if (evt.keyCode == 8) { // Backspace
177 if (searchStr.length > 0) {
178 searchStr = searchStr.substring(0, searchStr.length - 1);
179 this.txtNode_.value = searchStr;
180 this.beginSearch_(searchStr);
182 cvox.ChromeVox.navigationManager.updateSelToArbitraryNode(
184 cvox.ChromeVox.navigationManager.syncAll();
186 } else if (evt.keyCode == 40) { // Down arrow
187 this.next_(searchStr, false);
188 } else if (evt.keyCode == 38) { // Up arrow
189 this.next_(searchStr, true);
190 } else if (evt.keyCode == 13) { // Enter
192 } else if (evt.keyCode == 27) { // Escape
194 } else if (evt.ctrlKey && evt.keyCode == 67) { // ctrl + c
195 this.toggleCaseSensitivity_();
197 return goog.base(this, 'onKeyDown', evt);
199 evt.preventDefault();
200 evt.stopPropagation();
206 * Adds the letter the user typed to the search string and updates the search.
209 cvox.SearchWidget.prototype.onKeyPress = function(evt) {
210 if (!this.isActive()) {
214 this.txtNode_.value += String.fromCharCode(evt.charCode);
215 var searchStr = this.txtNode_.value;
216 this.beginSearch_(searchStr);
217 evt.preventDefault();
218 evt.stopPropagation();
224 * Called when navigation occurs.
225 * Override this method to react to navigation caused by user input.
227 cvox.SearchWidget.prototype.onNavigate = function() {
232 * Gets the predicate to apply to every search.
233 * @return {?function(Array.<Node>)} A predicate; if null, no predicate applies.
235 cvox.SearchWidget.prototype.getPredicate = function() {
241 * Goes to the next or previous result. For use in AndroidVox.
242 * @param {boolean=} opt_reverse Whether to find the next result in reverse.
243 * @return {Array.<cvox.NavDescription>} The next result.
245 cvox.SearchWidget.prototype.nextResult = function(opt_reverse) {
246 if (!this.isActive()) {
249 var searchStr = this.txtNode_.value;
250 return this.next_(searchStr, opt_reverse);
255 * Create the container node for the search overlay.
257 * @return {!Element} The new element, not yet added to the document.
260 cvox.SearchWidget.prototype.createContainerNode_ = function() {
261 var containerNode = document.createElement('div');
262 containerNode.id = 'cvox-search';
263 containerNode.style['position'] = 'fixed';
264 containerNode.style['top'] = '50%';
265 containerNode.style['left'] = '50%';
266 containerNode.style['-webkit-transition'] = 'all 0.3s ease-in';
267 containerNode.style['opacity'] = '0.0';
268 containerNode.style['z-index'] = '2147483647';
269 containerNode.setAttribute('aria-hidden', 'true');
270 return containerNode;
275 * Create the search overlay. This should be a child of the node
276 * returned from createContainerNode.
278 * @return {!Element} The new element, not yet added to the document.
281 cvox.SearchWidget.prototype.createOverlayNode_ = function() {
282 var overlayNode = document.createElement('div');
283 overlayNode.style['position'] = 'relative';
284 overlayNode.style['left'] = '-50%';
285 overlayNode.style['top'] = '-40px';
286 overlayNode.style['line-height'] = '1.2em';
287 overlayNode.style['font-size'] = '20px';
288 overlayNode.style['padding'] = '30px';
289 overlayNode.style['min-width'] = '150px';
290 overlayNode.style['color'] = '#fff';
291 overlayNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)';
292 overlayNode.style['border-radius'] = '10px';
298 * Create the text area node. This should be the child of the node
299 * returned from createOverlayNode.
301 * @return {!Element} The new element, not yet added to the document.
304 cvox.SearchWidget.prototype.createTextAreaNode_ = function() {
305 var textNode = document.createElement('textarea');
306 textNode.setAttribute('aria-hidden', 'true');
307 textNode.setAttribute('rows', '1');
308 textNode.style['color'] = '#fff';
309 textNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)';
310 textNode.style['vertical-align'] = 'middle';
311 textNode.addEventListener('textInput',
312 this.handleSearchChanged_, false);
318 * Toggles whether or not searches are case sensitive.
321 cvox.SearchWidget.prototype.toggleCaseSensitivity_ = function() {
322 if (this.caseSensitive_) {
323 cvox.SearchWidget.caseSensitive_ = false;
324 cvox.ChromeVox.tts.speak(
325 cvox.ChromeVox.msgs.getMsg('ignoring_case'),
326 cvox.QueueMode.FLUSH, null);
328 this.caseSensitive_ = true;
329 cvox.ChromeVox.tts.speak(
330 cvox.ChromeVox.msgs.getMsg('case_sensitive'),
331 cvox.QueueMode.FLUSH, null);
337 * Gets the next result.
339 * @param {string} searchStr The text to search for.
340 * @return {Array.<cvox.NavDescription>} The next result, in the form of
344 cvox.SearchWidget.prototype.getNextResult_ = function(searchStr) {
345 var r = cvox.ChromeVox.navigationManager.isReversed();
346 if (!this.caseSensitive_) {
347 searchStr = searchStr.toLowerCase();
350 cvox.ChromeVox.navigationManager.setGranularity(
351 cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false);
354 if (this.getPredicate()) {
355 var retNode = this.getPredicate()(cvox.DomUtil.getAncestors(
356 cvox.ChromeVox.navigationManager.getCurrentNode()));
362 var descriptions = cvox.ChromeVox.navigationManager.getDescription();
363 for (var i = 0; i < descriptions.length; i++) {
364 var targetStr = this.caseSensitive_ ? descriptions[i].text :
365 descriptions[i].text.toLowerCase();
366 var targetIndex = targetStr.indexOf(searchStr);
368 // Surround search hit with pauses.
369 if (targetIndex != -1 && targetStr.length > searchStr.length) {
370 descriptions[i].text =
371 cvox.DomUtil.collapseWhitespace(
372 targetStr.substring(0, targetIndex)) +
373 ', ' + searchStr + ', ' +
374 targetStr.substring(targetIndex + searchStr.length);
375 descriptions[i].text =
376 cvox.DomUtil.collapseWhitespace(descriptions[i].text);
378 if (targetIndex != -1) {
382 cvox.ChromeVox.navigationManager.setReversed(r);
383 } while (cvox.ChromeVox.navigationManager.navigate(true,
384 cvox.NavigationShifter.GRANULARITIES.OBJECT));
389 * Performs the search starting from the initial position.
391 * @param {string} searchStr The text to search for.
394 cvox.SearchWidget.prototype.beginSearch_ = function(searchStr) {
395 var result = this.getNextResult_(searchStr);
396 this.outputSearchResult_(result, searchStr);
402 * Goes to the next (directed) matching result.
404 * @param {string} searchStr The text to search for.
405 * @param {boolean=} opt_reversed The direction.
406 * @return {Array.<cvox.NavDescription>} The next result.
409 cvox.SearchWidget.prototype.next_ = function(searchStr, opt_reversed) {
410 cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed);
413 if (this.getPredicate()) {
414 success = cvox.ChromeVox.navigationManager.findNext(
415 /** @type {function(Array.<Node>)} */ (this.getPredicate()));
416 // TODO(dtseng): findNext always seems to point direction forward!
417 cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed);
419 cvox.ChromeVox.navigationManager.syncToBeginning();
420 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP);
424 success = cvox.ChromeVox.navigationManager.navigate(true);
426 var result = success ? this.getNextResult_(searchStr) : null;
427 this.outputSearchResult_(result, searchStr);
434 * Given a range corresponding to a search result, highlight the result,
435 * speak it, focus the node if applicable, and speak some instructions
438 * @param {Array.<cvox.NavDescription>} result The description of the next
439 * result. If null, no more results were found and an error will be presented.
440 * @param {string} searchStr The text to search for.
443 cvox.SearchWidget.prototype.outputSearchResult_ = function(result, searchStr) {
444 cvox.ChromeVox.tts.stop();
446 cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP);
447 this.hasMatch_ = false;
451 this.hasMatch_ = true;
453 // Speak the modified description and some instructions.
454 cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind(
455 cvox.ChromeVox.navigationManager.syncAll,
456 cvox.ChromeVox.navigationManager))(true);
458 cvox.ChromeVox.navigationManager.speakDescriptionArray(
460 cvox.QueueMode.FLUSH,
462 cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT);
464 cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('search_help_item'),
465 cvox.QueueMode.QUEUE,
466 cvox.AbstractTts.PERSONALITY_ANNOTATION);
468 // Output to Braille.
469 // TODO: Use line granularity in search so we can simply call
470 // cvox.ChromeVox.navigationManager.getBraille().write() instead.
471 this.outputSearchResultToBraille_(searchStr);
476 * Writes the currently selected search result to Braille, with description
477 * text formatted for Braille display instead of speech.
479 * @param {string} searchStr The text to search for.
480 * Should be in navigation manager's description.
483 cvox.SearchWidget.prototype.outputSearchResultToBraille_ = function(searchStr) {
484 // Construct object we can pass to Chromevox.braille to write.
485 // We concatenate the text together and set the "cursor"
486 // position to be at the end of search query string
487 // (consistent with editing text in a field).
488 var text = this.textFromCurrentDescription_();
489 var targetStr = this.caseSensitive_ ? text :
491 searchStr = this.caseSensitive_ ? searchStr : searchStr.toLowerCase();
492 var targetIndex = targetStr.indexOf(searchStr);
493 if (targetIndex == -1) {
494 console.log('Search string not in result when preparing for Braille.');
498 // Mark the string as a search result by adding a prefix
499 // and adjust the targetIndex accordingly.
500 var oldLength = text.length;
501 text = cvox.ChromeVox.msgs.getMsg('mark_as_search_result_brl', [text]);
502 var newLength = text.length;
503 targetIndex += (newLength - oldLength);
505 // Write to Braille with cursor at the end of the search hit.
506 cvox.ChromeVox.braille.write(new cvox.NavBraille({
508 startIndex: (targetIndex + searchStr.length),
509 endIndex: (targetIndex + searchStr.length)
515 * Returns the concatenated text from the current description in the
517 * TODO: May not be needed after we just simply use line granularity in search,
518 * since this is mostly used to display the long search result descriptions on
520 * @return {string} The concatenated text from the current description.
523 cvox.SearchWidget.prototype.textFromCurrentDescription_ = function() {
524 var descriptions = cvox.ChromeVox.navigationManager.getDescription();
526 for (var i = 0; i < descriptions.length; i++) {
527 text += descriptions[i].text + ' ';
533 * @param {Object} evt The onInput event that the function is handling.
536 cvox.SearchWidget.prototype.handleSearchChanged_ = function(evt) {
537 var searchStr = evt.target.value + evt.data;
538 cvox.SearchWidget.prototype.beginSearch_(searchStr);