| // 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_ = ''; |
| }; |