// 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} * @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.isEmptySafe(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.isEmptySafe(labelText)) { labelText = labelText.toLowerCase(); var nodeList = /** @type {Array} */ (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 = /** @type {Array} */ (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 = /** @type {Array} */ (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>?} 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_ = ''; };