blob: 91d22b1d483b6489ab4e6486316944c676e1d061 [file] [log] [blame]
// Copyright 2016 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 Objects related to incremental search.
*/
goog.provide('ISearch');
goog.provide('ISearchHandler');
goog.provide('ISearchUI');
goog.require('AutomationUtil');
goog.require('ChromeVoxState');
goog.require('Output');
goog.require('constants');
goog.require('cursors.Cursor');
goog.scope(function() {
var AutomationNode = chrome.automation.AutomationNode;
var Dir = constants.Dir;
var RoleType = chrome.automation.RoleType;
/**
* An interface implemented by objects that wish to handle events related to
* incremental search.
* @interface
*/
ISearchHandler = function() {};
ISearchHandler.prototype = {
/**
* Called when there are no remaining nodes in the document matching
* search.
* @param {!AutomationNode} boundaryNode The last node before reaching either
* the start or end of the document.
*/
onSearchReachedBoundary: function(boundaryNode) {},
/**
* Called when search result node changes.
* @param {!AutomationNode} node The new search result.
* @param {number} start The index into the name where the search match
* starts.
* @param {number} end The index into the name where the search match ends.
*/
onSearchResultChanged: function(node, start, end) {}
};
/**
* Controls an incremental search.
* @param {!cursors.Cursor} cursor
* @constructor
*/
ISearch = function(cursor) {
if (!cursor.node)
throw 'Incremental search started from invalid range.';
var leaf = AutomationUtil.findNodePre(
cursor.node, Dir.FORWARD, AutomationPredicate.leaf) ||
cursor.node;
/** @type {!cursors.Cursor} */
this.cursor = cursors.Cursor.fromNode(leaf);
/** @private {number} */
this.callbackId_ = 0;
// Global exports.
/** Exported for this background script. */
cvox.ChromeVox = chrome.extension.getBackgroundPage()['cvox']['ChromeVox'];
};
ISearch.prototype = {
/**
* @param {!ISearchHandler} handler
*/
set handler(handler) {
this.handler_ = handler;
},
/**
* Performs a search.
* @param {string} searchStr
* @param {Dir} dir
* @param {boolean=} opt_nextObject
*/
search: function(searchStr, dir, opt_nextObject) {
clearTimeout(this.callbackId_);
var step = function() {
searchStr = searchStr.toLocaleLowerCase();
var node = this.cursor.node;
var result = node;
if (opt_nextObject) {
// We want to start/continue the search at the next object.
result =
AutomationUtil.findNextNode(node, dir, AutomationPredicate.object);
}
do {
// Ask native to search the underlying data for a performance boost.
result = result.getNextTextMatch(searchStr, dir == Dir.BACKWARD);
} while (result && !AutomationPredicate.object(result));
if (result) {
this.cursor = cursors.Cursor.fromNode(result);
var start = result.name.toLocaleLowerCase().indexOf(searchStr);
var end = start + searchStr.length;
this.handler_.onSearchResultChanged(result, start, end);
} else {
this.handler_.onSearchReachedBoundary(this.cursor.node);
}
};
this.callbackId_ = setTimeout(step.bind(this), 0);
},
clear: function() {
clearTimeout(this.callbackId_);
}
};
/**
* @param {Element} input
* @constructor
* @implements {ISearchHandler}
*/
ISearchUI = function(input) {
/** @type {ChromeVoxState} @private */
this.background_ =
chrome.extension.getBackgroundPage()['ChromeVoxState']['instance'];
this.iSearch_ = new ISearch(this.background_.currentRange.start);
this.input_ = input;
this.dir_ = Dir.FORWARD;
this.iSearch_.handler = this;
this.onKeyDown = this.onKeyDown.bind(this);
this.onTextInput = this.onTextInput.bind(this);
input.addEventListener('keydown', this.onKeyDown, true);
input.addEventListener('textInput', this.onTextInput, false);
};
/**
* @param {Element} input
* @return {ISearchUI}
*/
ISearchUI.init = function(input) {
if (ISearchUI.instance_)
ISearchUI.instance_.destroy();
if (!input)
throw 'Expected search input';
ISearchUI.instance_ = new ISearchUI(input);
input.focus();
input.select();
return ISearchUI.instance_;
};
ISearchUI.prototype = {
/**
* Listens to key down events.
* @param {Event} evt
* @return {boolean}
*/
onKeyDown: function(evt) {
switch (evt.key) {
case 'ArrowUp':
this.dir_ = Dir.BACKWARD;
break;
case 'ArrowDown':
this.dir_ = Dir.FORWARD;
break;
case 'Escape':
Panel.closeMenusAndRestoreFocus();
return false;
case 'Enter':
Panel.setPendingCallback(function() {
var node = this.iSearch_.cursor.node;
if (!node)
return;
chrome.extension.getBackgroundPage()
.ChromeVoxState.instance['navigateToRange'](
cursors.Range.fromNode(node));
}.bind(this));
Panel.closeMenusAndRestoreFocus();
return false;
default:
return false;
}
this.iSearch_.search(this.input_.value, this.dir_, true);
evt.preventDefault();
evt.stopPropagation();
return false;
},
/**
* Listens to text input events.
* @param {Event} evt
* @return {boolean}
*/
onTextInput: function(evt) {
var searchStr = evt.target.value + evt.data;
this.iSearch_.clear();
this.iSearch_.search(searchStr, this.dir_);
return true;
},
/**
* @override
*/
onSearchReachedBoundary: function(boundaryNode) {
this.output_(boundaryNode);
cvox.ChromeVox.earcons.playEarcon(cvox.Earcon.WRAP);
},
/**
* @override
*/
onSearchResultChanged: function(node, start, end) {
this.output_(node, start, end);
},
/**
* @param {!AutomationNode} node
* @param {number=} opt_start
* @param {number=} opt_end
* @private
*/
output_: function(node, opt_start, opt_end) {
Output.forceModeForNextSpeechUtterance(cvox.QueueMode.FLUSH);
var o = new Output();
if (opt_start && opt_end) {
o.withString([
node.name.substr(0, opt_start),
node.name.substr(opt_start, opt_end - opt_start),
node.name.substr(opt_end)
].join(', '));
o.format('$role', node);
} else {
o.withRichSpeechAndBraille(
cursors.Range.fromNode(node), null, Output.EventType.NAVIGATE);
}
o.go();
this.background_.setCurrentRange(cursors.Range.fromNode(node));
},
/** Unregisters event handlers. */
destroy: function() {
this.iSearch_.handler_ = null;
this.iSearch_ = null;
var input = this.input_;
this.input_ = null;
input.removeEventListener('keydown', this.onKeyDown, true);
input.removeEventListener('textInput', this.onTextInput, false);
}
};
}); // goog.scope