summaryrefslogtreecommitdiffstats
path: root/chromium/chrome/browser/resources/chromeos/chromevox/chromevox/injected/ui/search_widget.js
diff options
context:
space:
mode:
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.js535
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);
+};