diff options
Diffstat (limited to 'chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js')
-rw-r--r-- | chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js | 535 |
1 files changed, 535 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js b/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js new file mode 100644 index 00000000000..7f50592c470 --- /dev/null +++ b/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js @@ -0,0 +1,535 @@ +// Copyright 2014 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/** + * @fileoverview JavaScript for poppup up a search widget and performing + * search within a page. + */ + +goog.provide('cvox.SearchWidget'); + +goog.require('cvox.AbstractEarcons'); +goog.require('cvox.ApiImplementation'); +goog.require('cvox.ChromeVox'); +goog.require('cvox.CursorSelection'); +goog.require('cvox.NavigationManager'); +goog.require('cvox.Widget'); + + +/** + * Initializes the search widget. + * @constructor + * @extends {cvox.Widget} + */ +cvox.SearchWidget = function() { + /** + * @type {Element} + * @private + */ + this.containerNode_ = null; + + /** + * @type {Element} + * @private + */ + this.txtNode_ = null; + + /** + * @type {string} + * @const + * @private + */ + this.PROMPT_ = 'Search:'; + + /** + * @type {boolean} + * @private + */ + this.caseSensitive_ = false; + + /** + * @type {boolean} + * @private + */ + this.hasMatch_ = false; + goog.base(this); +}; +goog.inherits(cvox.SearchWidget, cvox.Widget); +goog.addSingletonGetter(cvox.SearchWidget); + + +/** + * @override + */ +cvox.SearchWidget.prototype.show = function() { + goog.base(this, 'show'); + this.active = true; + this.hasMatch_ = false; + cvox.ChromeVox.navigationManager.setGranularity( + cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false); + + // Always start search forward. + cvox.ChromeVox.navigationManager.setReversed(false); + + // During profiling, NavigationHistory was found to have a serious performance + // impact on search. + this.focusRecovery_ = cvox.ChromeVox.navigationManager.getFocusRecovery(); + cvox.ChromeVox.navigationManager.setFocusRecovery(false); + + var containerNode = this.createContainerNode_(); + this.containerNode_ = containerNode; + + var overlayNode = this.createOverlayNode_(); + containerNode.appendChild(overlayNode); + + var promptNode = document.createElement('span'); + promptNode.innerHTML = this.PROMPT_; + overlayNode.appendChild(promptNode); + + this.txtNode_ = this.createTextAreaNode_(); + + overlayNode.appendChild(this.txtNode_); + + document.body.appendChild(containerNode); + + this.txtNode_.focus(); + + window.setTimeout(function() { + containerNode.style['opacity'] = '1.0'; + }, 0); +}; + + +/** + * @override + */ +cvox.SearchWidget.prototype.hide = function(opt_noSync) { + if (this.isActive()) { + var containerNode = this.containerNode_; + containerNode.style.opacity = '0.0'; + window.setTimeout(function() { + document.body.removeChild(containerNode); + }, 1000); + this.txtNode_ = null; + cvox.SearchWidget.containerNode = null; + cvox.ChromeVox.navigationManager.setFocusRecovery(this.focusRecovery_); + this.active = false; + } + + cvox.$m('choice_widget_exited'). + andPause(). + andMessage(this.getNameMsg()). + speakFlush(); + + if (!this.hasMatch_ || !opt_noSync) { + cvox.ChromeVox.navigationManager.updateSelToArbitraryNode( + this.initialNode); + } + cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind( + cvox.ChromeVox.navigationManager.syncAll, + cvox.ChromeVox.navigationManager))(true); + cvox.ChromeVox.navigationManager.speakDescriptionArray( + cvox.ChromeVox.navigationManager.getDescription(), + cvox.AbstractTts.QUEUE_MODE_QUEUE, + null, + cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); + + // Update on Braille too. + // TODO: Use line granularity in search so we can simply call + // cvox.ChromeVox.navigationManager.getBraille().write() instead. + var text = this.textFromCurrentDescription_(); + cvox.ChromeVox.braille.write(new cvox.NavBraille({ + text: text, + startIndex: 0, + endIndex: 0 + })); + + goog.base(this, 'hide', true); +}; + + +/** + * @override + */ +cvox.SearchWidget.prototype.getNameMsg = function() { + return ['search_widget_intro']; +}; + + +/** + * @override + */ +cvox.SearchWidget.prototype.getHelpMsg = function() { + return 'search_widget_intro_help'; +}; + + +/** + * @override + */ +cvox.SearchWidget.prototype.onKeyDown = function(evt) { + if (!this.isActive()) { + return false; + } + var searchStr = this.txtNode_.value; + if (evt.keyCode == 8) { // Backspace + if (searchStr.length > 0) { + searchStr = searchStr.substring(0, searchStr.length - 1); + this.txtNode_.value = searchStr; + this.beginSearch_(searchStr); + } else { + cvox.ChromeVox.navigationManager.updateSelToArbitraryNode( + this.initialNode); + cvox.ChromeVox.navigationManager.syncAll(); + } + } else if (evt.keyCode == 40) { // Down arrow + this.next_(searchStr, false); + } else if (evt.keyCode == 38) { // Up arrow + this.next_(searchStr, true); + } else if (evt.keyCode == 13) { // Enter + this.hide(true); + } else if (evt.keyCode == 27) { // Escape + this.hide(false); + } else if (evt.ctrlKey && evt.keyCode == 67) { // ctrl + c + this.toggleCaseSensitivity_(); + } else { + return goog.base(this, 'onKeyDown', evt); + } + evt.preventDefault(); + evt.stopPropagation(); + return true; +}; + + +/** + * Adds the letter the user typed to the search string and updates the search. + * @override + */ +cvox.SearchWidget.prototype.onKeyPress = function(evt) { + if (!this.isActive()) { + return false; + } + + this.txtNode_.value += String.fromCharCode(evt.charCode); + var searchStr = this.txtNode_.value; + this.beginSearch_(searchStr); + evt.preventDefault(); + evt.stopPropagation(); + return true; +}; + + +/** + * Called when navigation occurs. + * Override this method to react to navigation caused by user input. + */ +cvox.SearchWidget.prototype.onNavigate = function() { +}; + + +/** + * Gets the predicate to apply to every search. + * @return {?function(Array.<Node>)} A predicate; if null, no predicate applies. + */ +cvox.SearchWidget.prototype.getPredicate = function() { + return null; +}; + + +/** + * Goes to the next or previous result. For use in AndroidVox. + * @param {boolean=} opt_reverse Whether to find the next result in reverse. + * @return {Array.<cvox.NavDescription>} The next result. + */ +cvox.SearchWidget.prototype.nextResult = function(opt_reverse) { + if (!this.isActive()) { + return null; + } + var searchStr = this.txtNode_.value; + return this.next_(searchStr, opt_reverse); +}; + + +/** + * Create the container node for the search overlay. + * + * @return {!Element} The new element, not yet added to the document. + * @private + */ +cvox.SearchWidget.prototype.createContainerNode_ = function() { + var containerNode = document.createElement('div'); + containerNode.id = 'cvox-search'; + containerNode.style['position'] = 'fixed'; + containerNode.style['top'] = '50%'; + containerNode.style['left'] = '50%'; + containerNode.style['-webkit-transition'] = 'all 0.3s ease-in'; + containerNode.style['opacity'] = '0.0'; + containerNode.style['z-index'] = '2147483647'; + containerNode.setAttribute('aria-hidden', 'true'); + return containerNode; +}; + + +/** + * Create the search overlay. This should be a child of the node + * returned from createContainerNode. + * + * @return {!Element} The new element, not yet added to the document. + * @private + */ +cvox.SearchWidget.prototype.createOverlayNode_ = function() { + var overlayNode = document.createElement('div'); + overlayNode.style['position'] = 'relative'; + overlayNode.style['left'] = '-50%'; + overlayNode.style['top'] = '-40px'; + overlayNode.style['line-height'] = '1.2em'; + overlayNode.style['font-size'] = '20px'; + overlayNode.style['padding'] = '30px'; + overlayNode.style['min-width'] = '150px'; + overlayNode.style['color'] = '#fff'; + overlayNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)'; + overlayNode.style['border-radius'] = '10px'; + return overlayNode; +}; + + +/** + * Create the text area node. This should be the child of the node + * returned from createOverlayNode. + * + * @return {!Element} The new element, not yet added to the document. + * @private + */ +cvox.SearchWidget.prototype.createTextAreaNode_ = function() { + var textNode = document.createElement('textarea'); + textNode.setAttribute('aria-hidden', 'true'); + textNode.setAttribute('rows', '1'); + textNode.style['color'] = '#fff'; + textNode.style['background-color'] = 'rgba(0, 0, 0, 0.7)'; + textNode.style['vertical-align'] = 'middle'; + textNode.addEventListener('textInput', + this.handleSearchChanged_, false); + return textNode; +}; + + +/** + * Toggles whether or not searches are case sensitive. + * @private + */ +cvox.SearchWidget.prototype.toggleCaseSensitivity_ = function() { + if (this.caseSensitive_) { + cvox.SearchWidget.caseSensitive_ = false; + cvox.ChromeVox.tts.speak('Ignoring case.', 0, null); + } else { + this.caseSensitive_ = true; + cvox.ChromeVox.tts.speak('Case sensitive.', 0, null); + } +}; + + +/** + * Gets the next result. + * + * @param {string} searchStr The text to search for. + * @return {Array.<cvox.NavDescription>} The next result, in the form of + * NavDescriptions. + * @private + */ +cvox.SearchWidget.prototype.getNextResult_ = function(searchStr) { + var r = cvox.ChromeVox.navigationManager.isReversed(); + if (!this.caseSensitive_) { + searchStr = searchStr.toLowerCase(); + } + + cvox.ChromeVox.navigationManager.setGranularity( + cvox.NavigationShifter.GRANULARITIES.OBJECT, true, false); + + do { + if (this.getPredicate()) { + var retNode = this.getPredicate()(cvox.DomUtil.getAncestors( + cvox.ChromeVox.navigationManager.getCurrentNode())); + if (!retNode) { + continue; + } + } + + var descriptions = cvox.ChromeVox.navigationManager.getDescription(); + for (var i = 0; i < descriptions.length; i++) { + var targetStr = this.caseSensitive_ ? descriptions[i].text : + descriptions[i].text.toLowerCase(); + var targetIndex = targetStr.indexOf(searchStr); + + // Surround search hit with pauses. + if (targetIndex != -1 && targetStr.length > searchStr.length) { + descriptions[i].text = + cvox.DomUtil.collapseWhitespace( + targetStr.substring(0, targetIndex)) + + ', ' + searchStr + ', ' + + targetStr.substring(targetIndex + searchStr.length); + descriptions[i].text = + cvox.DomUtil.collapseWhitespace(descriptions[i].text); + } + if (targetIndex != -1) { + return descriptions; + } + } + cvox.ChromeVox.navigationManager.setReversed(r); + } while (cvox.ChromeVox.navigationManager.navigate(true, + cvox.NavigationShifter.GRANULARITIES.OBJECT)); +}; + + +/** + * Performs the search starting from the initial position. + * + * @param {string} searchStr The text to search for. + * @private + */ +cvox.SearchWidget.prototype.beginSearch_ = function(searchStr) { + var result = this.getNextResult_(searchStr); + this.outputSearchResult_(result, searchStr); + this.onNavigate(); +}; + + +/** + * Goes to the next (directed) matching result. + * + * @param {string} searchStr The text to search for. + * @param {boolean=} opt_reversed The direction. + * @return {Array.<cvox.NavDescription>} The next result. + * @private + */ +cvox.SearchWidget.prototype.next_ = function(searchStr, opt_reversed) { + cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed); + + var success = false; + if (this.getPredicate()) { + success = cvox.ChromeVox.navigationManager.findNext( + /** @type {function(Array.<Node>)} */ (this.getPredicate())); + // TODO(dtseng): findNext always seems to point direction forward! + cvox.ChromeVox.navigationManager.setReversed(!!opt_reversed); + if (!success) { + cvox.ChromeVox.navigationManager.syncToBeginning(); + cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); + success = true; + } + } else { + success = cvox.ChromeVox.navigationManager.navigate(true); + } + var result = success ? this.getNextResult_(searchStr) : null; + this.outputSearchResult_(result, searchStr); + this.onNavigate(); + return result; +}; + + +/** + * Given a range corresponding to a search result, highlight the result, + * speak it, focus the node if applicable, and speak some instructions + * at the end. + * + * @param {Array.<cvox.NavDescription>} result The description of the next + * result. If null, no more results were found and an error will be presented. + * @param {string} searchStr The text to search for. + * @private + */ +cvox.SearchWidget.prototype.outputSearchResult_ = function(result, searchStr) { + cvox.ChromeVox.tts.stop(); + if (!result) { + cvox.ChromeVox.earcons.playEarcon(cvox.AbstractEarcons.WRAP); + this.hasMatch_ = false; + return; + } + + this.hasMatch_ = true; + + // Speak the modified description and some instructions. + cvox.ChromeVoxEventSuspender.withSuspendedEvents(goog.bind( + cvox.ChromeVox.navigationManager.syncAll, + cvox.ChromeVox.navigationManager))(true); + + cvox.ChromeVox.navigationManager.speakDescriptionArray( + result, + cvox.AbstractTts.QUEUE_MODE_FLUSH, + null, + cvox.AbstractTts.PERSONALITY_ANNOUNCEMENT); + + cvox.ChromeVox.tts.speak(cvox.ChromeVox.msgs.getMsg('search_help_item'), + cvox.AbstractTts.QUEUE_MODE_QUEUE, + cvox.AbstractTts.PERSONALITY_ANNOTATION); + + // Output to Braille. + // TODO: Use line granularity in search so we can simply call + // cvox.ChromeVox.navigationManager.getBraille().write() instead. + this.outputSearchResultToBraille_(searchStr); +}; + + +/** + * Writes the currently selected search result to Braille, with description + * text formatted for Braille display instead of speech. + * + * @param {string} searchStr The text to search for. + * Should be in navigation manager's description. + * @private + */ +cvox.SearchWidget.prototype.outputSearchResultToBraille_ = function(searchStr) { + // Construct object we can pass to Chromevox.braille to write. + // We concatenate the text together and set the "cursor" + // position to be at the end of search query string + // (consistent with editing text in a field). + var text = this.textFromCurrentDescription_(); + var targetStr = this.caseSensitive_ ? text : + text.toLowerCase(); + searchStr = this.caseSensitive_ ? searchStr : searchStr.toLowerCase(); + var targetIndex = targetStr.indexOf(searchStr); + if (targetIndex == -1) { + console.log('Search string not in result when preparing for Braille.'); + return; + } + + // Mark the string as a search result by adding a prefix + // and adjust the targetIndex accordingly. + var oldLength = text.length; + text = cvox.ChromeVox.msgs.getMsg('mark_as_search_result_brl', [text]); + var newLength = text.length; + targetIndex += (newLength - oldLength); + + // Write to Braille with cursor at the end of the search hit. + cvox.ChromeVox.braille.write(new cvox.NavBraille({ + text: text, + startIndex: (targetIndex + searchStr.length), + endIndex: (targetIndex + searchStr.length) + })); +}; + + +/** + * Returns the concatenated text from the current description in the + * NavigationManager. + * TODO: May not be needed after we just simply use line granularity in search, + * since this is mostly used to display the long search result descriptions on + * Braille. + * @return {string} The concatenated text from the current description. + * @private + */ +cvox.SearchWidget.prototype.textFromCurrentDescription_ = function() { + var descriptions = cvox.ChromeVox.navigationManager.getDescription(); + var text = ''; + for (var i = 0; i < descriptions.length; i++) { + text += descriptions[i].text + ' '; + } + return text; +}; + +/** + * @param {Object} evt The onInput event that the function is handling. + * @private + */ +cvox.SearchWidget.prototype.handleSearchChanged_ = function(evt) { + var searchStr = evt.target.value + evt.data; + cvox.SearchWidget.prototype.beginSearch_(searchStr); +}; |