123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279 |
- // Copyright 2013 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 Matcher which maintains a client-side cache on top of some
- * other matcher.
- * @author reinerp@google.com (Reiner Pope)
- */
- goog.provide('goog.ui.ac.CachingMatcher');
- goog.require('goog.array');
- goog.require('goog.async.Throttle');
- goog.require('goog.ui.ac.ArrayMatcher');
- goog.require('goog.ui.ac.RenderOptions');
- /**
- * A matcher which wraps another (typically slow) matcher and
- * keeps a client-side cache of the results. For instance, you can use this to
- * wrap a RemoteArrayMatcher to hide the latency of the underlying matcher
- * having to make ajax request.
- *
- * Objects in the cache are deduped on their stringified forms.
- *
- * Note - when the user types a character, they will instantly get a set of
- * local results, and then some time later, the results from the server will
- * show up.
- *
- * @constructor
- * @param {!Object} baseMatcher The underlying matcher to use. Must implement
- * requestMatchingRows.
- * @final
- */
- goog.ui.ac.CachingMatcher = function(baseMatcher) {
- /** @private {!Array<!Object>}} The cache. */
- this.rows_ = [];
- /**
- * Set of stringified rows, for fast deduping. Each element of this.rows_
- * is stored in rowStrings_ as (' ' + row) to ensure we avoid builtin
- * properties like 'toString'.
- * @private {Object<string, boolean>}
- */
- this.rowStrings_ = {};
- /**
- * Maximum number of rows in the cache. If the cache grows larger than this,
- * the entire cache will be emptied.
- * @private {number}
- */
- this.maxCacheSize_ = 1000;
- /** @private {!Object} The underlying matcher to use. */
- this.baseMatcher_ = baseMatcher;
- /**
- * Local matching function.
- * @private {function(string, number, !Array<!Object>): !Array<!Object>}
- */
- this.getMatchesForRows_ = goog.ui.ac.ArrayMatcher.getMatchesForRows;
- /** @private {number} Number of matches to request from the base matcher. */
- this.baseMatcherMaxMatches_ = 100;
- /** @private {goog.async.Throttle} */
- this.throttledTriggerBaseMatch_ =
- new goog.async.Throttle(this.triggerBaseMatch_, 150, this);
- /** @private {string} */
- this.mostRecentToken_ = '';
- /** @private {Function} */
- this.mostRecentMatchHandler_ = null;
- /** @private {number} */
- this.mostRecentMaxMatches_ = 10;
- /**
- * The set of rows which we last displayed.
- *
- * NOTE(reinerp): The need for this is subtle. When a server result comes
- * back, we don't want to suddenly change the list of results without the user
- * doing anything. So we make sure to add the new server results to the end of
- * the currently displayed list.
- *
- * We need to keep track of the last rows we displayed, because the "similar
- * matcher" we use locally might otherwise reorder results.
- *
- * @private {Array<!Object>}
- */
- this.mostRecentMatches_ = [];
- };
- /**
- * Sets the number of milliseconds with which to throttle the match requests
- * on the underlying matcher.
- *
- * Default value: 150.
- *
- * @param {number} throttleTime .
- */
- goog.ui.ac.CachingMatcher.prototype.setThrottleTime = function(throttleTime) {
- this.throttledTriggerBaseMatch_ =
- new goog.async.Throttle(this.triggerBaseMatch_, throttleTime, this);
- };
- /**
- * Sets the maxMatches to use for the base matcher. If the base matcher makes
- * AJAX requests, it may help to make this a large number so that the local
- * cache gets populated quickly.
- *
- * Default value: 100.
- *
- * @param {number} maxMatches The value to set.
- */
- goog.ui.ac.CachingMatcher.prototype.setBaseMatcherMaxMatches = function(
- maxMatches) {
- this.baseMatcherMaxMatches_ = maxMatches;
- };
- /**
- * Sets the maximum size of the local cache. If the local cache grows larger
- * than this size, it will be emptied.
- *
- * Default value: 1000.
- *
- * @param {number} maxCacheSize .
- */
- goog.ui.ac.CachingMatcher.prototype.setMaxCacheSize = function(maxCacheSize) {
- this.maxCacheSize_ = maxCacheSize;
- };
- /**
- * Sets the local matcher to use.
- *
- * The local matcher should be a function with the same signature as
- * {@link goog.ui.ac.ArrayMatcher.getMatchesForRows}, i.e. its arguments are
- * searchToken, maxMatches, rowsToSearch; and it returns a list of matching
- * rows.
- *
- * Default value: {@link goog.ui.ac.ArrayMatcher.getMatchesForRows}.
- *
- * @param {function(string, number, !Array<!Object>): !Array<!Object>}
- * localMatcher
- */
- goog.ui.ac.CachingMatcher.prototype.setLocalMatcher = function(localMatcher) {
- this.getMatchesForRows_ = localMatcher;
- };
- /**
- * Function used to pass matches to the autocomplete.
- * @param {string} token Token to match.
- * @param {number} maxMatches Max number of matches to return.
- * @param {Function} matchHandler callback to execute after matching.
- */
- goog.ui.ac.CachingMatcher.prototype.requestMatchingRows = function(
- token, maxMatches, matchHandler) {
- this.mostRecentMaxMatches_ = maxMatches;
- this.mostRecentToken_ = token;
- this.mostRecentMatchHandler_ = matchHandler;
- this.throttledTriggerBaseMatch_.fire();
- var matches = this.getMatchesForRows_(token, maxMatches, this.rows_);
- matchHandler(token, matches);
- this.mostRecentMatches_ = matches;
- };
- /** Clears the cache. */
- goog.ui.ac.CachingMatcher.prototype.clearCache = function() {
- this.rows_ = [];
- this.rowStrings_ = {};
- };
- /**
- * Adds the specified rows to the cache.
- * @param {!Array<!Object>} rows .
- * @private
- */
- goog.ui.ac.CachingMatcher.prototype.addRows_ = function(rows) {
- goog.array.forEach(rows, function(row) {
- // The ' ' prefix is to avoid colliding with builtins like toString.
- if (!this.rowStrings_[' ' + row]) {
- this.rows_.push(row);
- this.rowStrings_[' ' + row] = true;
- }
- }, this);
- };
- /**
- * Checks if the cache is larger than the maximum cache size. If so clears it.
- * @private
- */
- goog.ui.ac.CachingMatcher.prototype.clearCacheIfTooLarge_ = function() {
- if (this.rows_.length > this.maxCacheSize_) {
- this.clearCache();
- }
- };
- /**
- * Triggers a match request against the base matcher. This function is
- * unthrottled, so don't call it directly; instead use
- * this.throttledTriggerBaseMatch_.
- * @private
- */
- goog.ui.ac.CachingMatcher.prototype.triggerBaseMatch_ = function() {
- this.baseMatcher_.requestMatchingRows(
- this.mostRecentToken_, this.baseMatcherMaxMatches_,
- goog.bind(this.onBaseMatch_, this));
- };
- /**
- * Handles a match response from the base matcher.
- * @param {string} token The token against which the base match was called.
- * @param {!Array<!Object>} matches The matches returned by the base matcher.
- * @private
- */
- goog.ui.ac.CachingMatcher.prototype.onBaseMatch_ = function(token, matches) {
- // NOTE(reinerp): The user might have typed some more characters since the
- // base matcher request was sent out, which manifests in that token might be
- // older than this.mostRecentToken_. We make sure to do our local matches
- // using this.mostRecentToken_ rather than token so that we display results
- // relevant to what the user is seeing right now.
- // NOTE(reinerp): We compute a diff between the currently displayed results
- // and the new results we would get now that the server results have come
- // back. Using this diff, we make sure the new results are only added to the
- // end of the list of results. See the documentation on
- // this.mostRecentMatches_ for details
- this.addRows_(matches);
- var oldMatchesSet = {};
- goog.array.forEach(this.mostRecentMatches_, function(match) {
- // The ' ' prefix is to avoid colliding with builtins like toString.
- oldMatchesSet[' ' + match] = true;
- });
- var newMatches = this.getMatchesForRows_(
- this.mostRecentToken_, this.mostRecentMaxMatches_, this.rows_);
- newMatches = goog.array.filter(
- newMatches, function(match) { return !(oldMatchesSet[' ' + match]); });
- newMatches = this.mostRecentMatches_.concat(newMatches)
- .slice(0, this.mostRecentMaxMatches_);
- this.mostRecentMatches_ = newMatches;
- // We've gone to the effort of keeping the existing rows as before, so let's
- // make sure to keep them highlighted.
- var options = new goog.ui.ac.RenderOptions();
- options.setPreserveHilited(true);
- this.mostRecentMatchHandler_(this.mostRecentToken_, newMatches, options);
- // We clear the cache *after* running the local match, so we don't
- // suddenly remove results just because the remote match came back.
- this.clearCacheIfTooLarge_();
- };
|