blob: 777c453a80529baca44db5d30d3b25199cf84e04 [file] [log] [blame]
// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @fileoverview Provides the typeahead functionality for the tree class.
*
*/
goog.provide('goog.ui.tree.TypeAhead');
goog.provide('goog.ui.tree.TypeAhead.Offset');
goog.require('goog.array');
goog.require('goog.events.KeyCodes');
goog.require('goog.string');
goog.require('goog.structs.Trie');
/**
* Constructs a TypeAhead object.
* @constructor
* @final
*/
goog.ui.tree.TypeAhead = function() {
this.nodeMap_ = new goog.structs.Trie();
};
/**
* Map of tree nodes to allow for quick access by characters in the label text.
* @type {goog.structs.Trie<Array<goog.ui.tree.BaseNode>>}
* @private
*/
goog.ui.tree.TypeAhead.prototype.nodeMap_;
/**
* Buffer for storing typeahead characters.
* @type {string}
* @private
*/
goog.ui.tree.TypeAhead.prototype.buffer_ = '';
/**
* Matching labels from the latest typeahead search.
* @type {?Array<string>}
* @private
*/
goog.ui.tree.TypeAhead.prototype.matchingLabels_ = null;
/**
* Matching nodes from the latest typeahead search. Used when more than
* one node is present with the same label text.
* @type {?Array<goog.ui.tree.BaseNode>}
* @private
*/
goog.ui.tree.TypeAhead.prototype.matchingNodes_ = null;
/**
* Specifies the current index of the label from the latest typeahead search.
* @type {number}
* @private
*/
goog.ui.tree.TypeAhead.prototype.matchingLabelIndex_ = 0;
/**
* Specifies the index into matching nodes when more than one node is found
* with the same label.
* @type {number}
* @private
*/
goog.ui.tree.TypeAhead.prototype.matchingNodeIndex_ = 0;
/**
* Enum for offset values that are used for ctrl-key navigation among the
* multiple matches of a given typeahead buffer.
*
* @enum {number}
*/
goog.ui.tree.TypeAhead.Offset = {
DOWN: 1,
UP: -1
};
/**
* Handles navigation keys.
* @param {goog.events.BrowserEvent} e The browser event.
* @return {boolean} The handled value.
*/
goog.ui.tree.TypeAhead.prototype.handleNavigation = function(e) {
var handled = false;
switch (e.keyCode) {
// Handle ctrl+down, ctrl+up to navigate within typeahead results.
case goog.events.KeyCodes.DOWN:
case goog.events.KeyCodes.UP:
if (e.ctrlKey) {
this.jumpTo_(e.keyCode == goog.events.KeyCodes.DOWN ?
goog.ui.tree.TypeAhead.Offset.DOWN :
goog.ui.tree.TypeAhead.Offset.UP);
handled = true;
}
break;
// Remove the last typeahead char.
case goog.events.KeyCodes.BACKSPACE:
var length = this.buffer_.length - 1;
handled = true;
if (length > 0) {
this.buffer_ = this.buffer_.substring(0, length);
this.jumpToLabel_(this.buffer_);
} else if (length == 0) {
// Clear the last character in typeahead.
this.buffer_ = '';
} else {
handled = false;
}
break;
// Clear typeahead buffer.
case goog.events.KeyCodes.ESC:
this.buffer_ = '';
handled = true;
break;
}
return handled;
};
/**
* Handles the character presses.
* @param {goog.events.BrowserEvent} e The browser event.
* Expected event type is goog.events.KeyHandler.EventType.KEY.
* @return {boolean} The handled value.
*/
goog.ui.tree.TypeAhead.prototype.handleTypeAheadChar = function(e) {
var handled = false;
if (!e.ctrlKey && !e.altKey) {
// Since goog.structs.Trie.getKeys compares characters during
// lookup, we should use charCode instead of keyCode where possible.
// Convert to lowercase, typeahead is case insensitive.
var ch = String.fromCharCode(e.charCode || e.keyCode).toLowerCase();
if (goog.string.isUnicodeChar(ch) && (ch != ' ' || this.buffer_)) {
this.buffer_ += ch;
handled = this.jumpToLabel_(this.buffer_);
}
}
return handled;
};
/**
* Adds or updates the given node in the nodemap. The label text is used as a
* key and the node id is used as a value. In the case that the key already
* exists, such as when more than one node exists with the same label, then this
* function creates an array to hold the multiple nodes.
* @param {goog.ui.tree.BaseNode} node Node to be added or updated.
*/
goog.ui.tree.TypeAhead.prototype.setNodeInMap = function(node) {
var labelText = node.getText();
if (labelText && !goog.string.isEmptyOrWhitespace(goog.string.makeSafe(labelText))) {
// Typeahead is case insensitive, convert to lowercase.
labelText = labelText.toLowerCase();
var previousValue = this.nodeMap_.get(labelText);
if (previousValue) {
// Found a previously created array, add the given node.
previousValue.push(node);
} else {
// Create a new array and set the array as value.
var nodeList = [node];
this.nodeMap_.set(labelText, nodeList);
}
}
};
/**
* Removes the given node from the nodemap.
* @param {goog.ui.tree.BaseNode} node Node to be removed.
*/
goog.ui.tree.TypeAhead.prototype.removeNodeFromMap = function(node) {
var labelText = node.getText();
if (labelText && !goog.string.isEmptyOrWhitespace(goog.string.makeSafe(labelText))) {
labelText = labelText.toLowerCase();
var nodeList = this.nodeMap_.get(labelText);
if (nodeList) {
// Remove the node from the array.
goog.array.remove(nodeList, node);
if (!!nodeList.length) {
this.nodeMap_.remove(labelText);
}
}
}
};
/**
* Select the first matching node for the given typeahead.
* @param {string} typeAhead Typeahead characters to match.
* @return {boolean} True iff a node is found.
* @private
*/
goog.ui.tree.TypeAhead.prototype.jumpToLabel_ = function(typeAhead) {
var handled = false;
var labels = this.nodeMap_.getKeys(typeAhead);
// Make sure we have at least one matching label.
if (labels && labels.length) {
this.matchingNodeIndex_ = 0;
this.matchingLabelIndex_ = 0;
var nodes = this.nodeMap_.get(labels[0]);
if ((handled = this.selectMatchingNode_(nodes))) {
this.matchingLabels_ = labels;
}
}
// TODO(user): beep when no node is found
return handled;
};
/**
* Select the next or previous node based on the offset.
* @param {goog.ui.tree.TypeAhead.Offset} offset DOWN or UP.
* @return {boolean} Whether a node is found.
* @private
*/
goog.ui.tree.TypeAhead.prototype.jumpTo_ = function(offset) {
var handled = false;
var labels = this.matchingLabels_;
if (labels) {
var nodes = null;
var nodeIndexOutOfRange = false;
// Navigate within the nodes array.
if (this.matchingNodes_) {
var newNodeIndex = this.matchingNodeIndex_ + offset;
if (newNodeIndex >= 0 && newNodeIndex < this.matchingNodes_.length) {
this.matchingNodeIndex_ = newNodeIndex;
nodes = this.matchingNodes_;
} else {
nodeIndexOutOfRange = true;
}
}
// Navigate to the next or previous label.
if (!nodes) {
var newLabelIndex = this.matchingLabelIndex_ + offset;
if (newLabelIndex >= 0 && newLabelIndex < labels.length) {
this.matchingLabelIndex_ = newLabelIndex;
}
if (labels.length > this.matchingLabelIndex_) {
nodes = this.nodeMap_.get(labels[this.matchingLabelIndex_]);
}
// Handle the case where we are moving beyond the available nodes,
// while going UP select the last item of multiple nodes with same label
// and while going DOWN select the first item of next set of nodes
if (nodes && nodes.length && nodeIndexOutOfRange) {
this.matchingNodeIndex_ = (offset == goog.ui.tree.TypeAhead.Offset.UP) ?
nodes.length - 1 : 0;
}
}
if ((handled = this.selectMatchingNode_(nodes))) {
this.matchingLabels_ = labels;
}
}
// TODO(user): beep when no node is found
return handled;
};
/**
* Given a nodes array reveals and selects the node while using node index.
* @param {Array<goog.ui.tree.BaseNode>|undefined} nodes Nodes array to select
* the node from.
* @return {boolean} Whether a matching node was found.
* @private
*/
goog.ui.tree.TypeAhead.prototype.selectMatchingNode_ = function(nodes) {
var node;
if (nodes) {
// Find the matching node.
if (this.matchingNodeIndex_ < nodes.length) {
node = nodes[this.matchingNodeIndex_];
this.matchingNodes_ = nodes;
}
if (node) {
node.reveal();
node.select();
}
}
return !!node;
};
/**
* Clears the typeahead buffer.
*/
goog.ui.tree.TypeAhead.prototype.clear = function() {
this.buffer_ = '';
};