Adding mapbox-gl branch
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// Copyright 2012 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 Utility methods supporting the autocomplete package.
|
||||
*
|
||||
* @see ../../demos/autocomplete-basic.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac');
|
||||
|
||||
goog.require('goog.ui.ac.ArrayMatcher');
|
||||
goog.require('goog.ui.ac.AutoComplete');
|
||||
goog.require('goog.ui.ac.InputHandler');
|
||||
goog.require('goog.ui.ac.Renderer');
|
||||
|
||||
|
||||
/**
|
||||
* Factory function for building a basic autocomplete widget that autocompletes
|
||||
* an inputbox or text area from a data array.
|
||||
* @param {Array<?>} data Data array.
|
||||
* @param {Element} input Input element or text area.
|
||||
* @param {boolean=} opt_multi Whether to allow multiple entries separated with
|
||||
* semi-colons or commas.
|
||||
* @param {boolean=} opt_useSimilar use similar matches. e.g. "gost" => "ghost".
|
||||
* @return {!goog.ui.ac.AutoComplete} A new autocomplete object.
|
||||
*/
|
||||
goog.ui.ac.createSimpleAutoComplete =
|
||||
function(data, input, opt_multi, opt_useSimilar) {
|
||||
var matcher = new goog.ui.ac.ArrayMatcher(data, !opt_useSimilar);
|
||||
var renderer = new goog.ui.ac.Renderer();
|
||||
var inputHandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi);
|
||||
|
||||
var autoComplete = new goog.ui.ac.AutoComplete(
|
||||
matcher, renderer, inputHandler);
|
||||
inputHandler.attachAutoComplete(autoComplete);
|
||||
inputHandler.attachInputs(input);
|
||||
return autoComplete;
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2007 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<!--
|
||||
|
||||
Integration tests for the entire autocomplete package.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.AutoComplete
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.acTest');
|
||||
</script>
|
||||
</head>
|
||||
<body id="body">
|
||||
<input type="text" id="input" />
|
||||
<input type="text" id="user" value="For manual testing" />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,209 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.ui.acTest');
|
||||
goog.setTestOnly('goog.ui.acTest');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.dom');
|
||||
goog.require('goog.dom.NodeType');
|
||||
goog.require('goog.dom.classlist');
|
||||
goog.require('goog.dom.selection');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.BrowserEvent');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventType');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.style');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ac');
|
||||
goog.require('goog.userAgent');
|
||||
var autocomplete;
|
||||
var data = ['ab', 'aab', 'aaab'];
|
||||
var input;
|
||||
var mockClock;
|
||||
|
||||
function setUpPage() {
|
||||
goog.ui.ac.createSimpleAutoComplete(data, goog.dom.getElement('user'), true,
|
||||
false);
|
||||
}
|
||||
|
||||
function setUp() {
|
||||
mockClock = new goog.testing.MockClock(true);
|
||||
input = goog.dom.getElement('input');
|
||||
input.value = '';
|
||||
autocomplete = goog.ui.ac.createSimpleAutoComplete(data, input, true, false);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
autocomplete.dispose();
|
||||
mockClock.dispose();
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// Utility methods
|
||||
|
||||
|
||||
/**
|
||||
* Fire listeners of a given type that are listening to the event's
|
||||
* currentTarget.
|
||||
*
|
||||
* @param {goog.events.BrowserEvent} event
|
||||
*/
|
||||
function simulateEvent(event) {
|
||||
goog.events.fireListeners(
|
||||
event.currentTarget, event.type, true, event);
|
||||
goog.events.fireListeners(
|
||||
event.currentTarget, event.type, false, event);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Fire all key event listeners that are listening to the input element.
|
||||
*
|
||||
* @param {number} keyCode The key code.
|
||||
*/
|
||||
function simulateAllKeyEventsOnInput(keyCode) {
|
||||
var eventTypes = [
|
||||
goog.events.EventType.KEYDOWN,
|
||||
goog.events.EventType.KEYPRESS,
|
||||
goog.events.EventType.KEYUP
|
||||
];
|
||||
|
||||
goog.array.forEach(eventTypes,
|
||||
function(type) {
|
||||
var event = new goog.events.Event(type, input);
|
||||
event.keyCode = keyCode;
|
||||
simulateEvent(new goog.events.BrowserEvent(event, input));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param {string} text
|
||||
* @return {Node} Node whose inner text maches the given text.
|
||||
*/
|
||||
function findNodeByInnerText(text) {
|
||||
return goog.dom.findNode(document.body, function(node) {
|
||||
try {
|
||||
var display = goog.userAgent.IE ?
|
||||
goog.style.getCascadedStyle(node, 'display') :
|
||||
goog.style.getComputedStyle(node, 'display');
|
||||
|
||||
return goog.dom.getRawTextContent(node) == text &&
|
||||
'none' != display && node.nodeType == goog.dom.NodeType.ELEMENT;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//=========================================================================
|
||||
// Tests
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that the display of the autocompleter works.
|
||||
*/
|
||||
function testBasicDisplay() {
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.DOWN);
|
||||
|
||||
input.value = 'a';
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.A);
|
||||
mockClock.tick(500);
|
||||
|
||||
var nodes = [
|
||||
findNodeByInnerText(data[0]),
|
||||
findNodeByInnerText(data[1]),
|
||||
findNodeByInnerText(data[2])
|
||||
];
|
||||
assert(!!nodes[0]);
|
||||
assert(!!nodes[1]);
|
||||
assert(!!nodes[2]);
|
||||
assert(goog.style.isUnselectable(nodes[0]));
|
||||
assert(goog.style.isUnselectable(nodes[1]));
|
||||
assert(goog.style.isUnselectable(nodes[2]));
|
||||
|
||||
input.value = 'aa';
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.A);
|
||||
mockClock.tick(500);
|
||||
|
||||
assertFalse(!!findNodeByInnerText(data[0]));
|
||||
assert(!!findNodeByInnerText(data[1]));
|
||||
assert(!!findNodeByInnerText(data[2]));
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that key navigation with multiple inputs work
|
||||
*/
|
||||
function testKeyNavigation() {
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.DOWN);
|
||||
|
||||
input.value = 'c, a';
|
||||
goog.dom.selection.setCursorPosition(input, 'c, a'.length);
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.A);
|
||||
mockClock.tick(500);
|
||||
|
||||
assert(document.body.innerHTML, !!findNodeByInnerText(data[1]));
|
||||
assert(!!findNodeByInnerText(data[2]));
|
||||
|
||||
var selected = goog.asserts.assertElement(findNodeByInnerText(data[0]));
|
||||
assertTrue('Should have new standard active class',
|
||||
goog.dom.classlist.contains(selected, 'ac-active'));
|
||||
assertTrue('Should have legacy active class',
|
||||
goog.dom.classlist.contains(selected, 'active'));
|
||||
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.DOWN);
|
||||
assertFalse(goog.dom.classlist.contains(
|
||||
goog.asserts.assertElement(findNodeByInnerText(data[0])), 'ac-active'));
|
||||
assert(goog.dom.classlist.contains(
|
||||
goog.asserts.assertElement(findNodeByInnerText(data[1])), 'ac-active'));
|
||||
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.ENTER);
|
||||
assertEquals('c, aab, ', input.value);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Ensure that mouse navigation with multiple inputs works.
|
||||
*/
|
||||
function testMouseNavigation() {
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.DOWN);
|
||||
|
||||
input.value = 'c, a';
|
||||
goog.dom.selection.setCursorPosition(input, 'c, a'.length);
|
||||
simulateAllKeyEventsOnInput(goog.events.KeyCodes.A);
|
||||
mockClock.tick(500);
|
||||
|
||||
var secondOption = goog.asserts.assertElement(findNodeByInnerText(data[1]));
|
||||
var parent = secondOption.parentNode;
|
||||
assertFalse(goog.dom.classlist.contains(secondOption, 'ac-active'));
|
||||
|
||||
var mouseOver = new goog.events.Event(
|
||||
goog.events.EventType.MOUSEOVER, secondOption);
|
||||
simulateEvent(new goog.events.BrowserEvent(mouseOver, parent));
|
||||
assert(goog.dom.classlist.contains(secondOption, 'ac-active'));
|
||||
|
||||
var mouseDown = new goog.events.Event(
|
||||
goog.events.EventType.MOUSEDOWN, secondOption);
|
||||
simulateEvent(new goog.events.BrowserEvent(mouseDown, parent));
|
||||
var mouseClick = new goog.events.Event(
|
||||
goog.events.EventType.CLICK, secondOption);
|
||||
simulateEvent(new goog.events.BrowserEvent(mouseClick, parent));
|
||||
|
||||
assertEquals('c, aab, ', input.value);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
// Copyright 2006 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 Basic class for matching words in an array.
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
goog.provide('goog.ui.ac.ArrayMatcher');
|
||||
|
||||
goog.require('goog.string');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Basic class for matching words in an array
|
||||
* @constructor
|
||||
* @param {Array<?>} rows Dictionary of items to match. Can be objects if they
|
||||
* have a toString method that returns the value to match against.
|
||||
* @param {boolean=} opt_noSimilar if true, do not do similarity matches for the
|
||||
* input token against the dictionary.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher = function(rows, opt_noSimilar) {
|
||||
this.rows_ = rows || [];
|
||||
this.useSimilar_ = !opt_noSimilar;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Replaces the rows that this object searches over.
|
||||
* @param {Array<?>} rows Dictionary of items to match.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.prototype.setRows = function(rows) {
|
||||
this.rows_ = rows || [];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param {string=} opt_fullString The full string from the input box.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.prototype.requestMatchingRows =
|
||||
function(token, maxMatches, matchHandler, opt_fullString) {
|
||||
|
||||
var matches = this.useSimilar_ ?
|
||||
goog.ui.ac.ArrayMatcher.getMatchesForRows(token, maxMatches, this.rows_) :
|
||||
this.getPrefixMatches(token, maxMatches);
|
||||
|
||||
matchHandler(token, matches);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Matches the token against the specified rows, first looking for prefix
|
||||
* matches and if that fails, then looking for similar matches.
|
||||
*
|
||||
* @param {string} token Token to match.
|
||||
* @param {number} maxMatches Max number of matches to return.
|
||||
* @param {!Array<?>} rows Rows to search for matches. Can be objects if they
|
||||
* have a toString method that returns the value to match against.
|
||||
* @return {!Array<?>} Rows that match.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.getMatchesForRows =
|
||||
function(token, maxMatches, rows) {
|
||||
var matches =
|
||||
goog.ui.ac.ArrayMatcher.getPrefixMatchesForRows(token, maxMatches, rows);
|
||||
|
||||
if (matches.length == 0) {
|
||||
matches = goog.ui.ac.ArrayMatcher.getSimilarMatchesForRows(token,
|
||||
maxMatches, rows);
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Matches the token against the start of words in the row.
|
||||
* @param {string} token Token to match.
|
||||
* @param {number} maxMatches Max number of matches to return.
|
||||
* @return {!Array<?>} Rows that match.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.prototype.getPrefixMatches =
|
||||
function(token, maxMatches) {
|
||||
return goog.ui.ac.ArrayMatcher.getPrefixMatchesForRows(token, maxMatches,
|
||||
this.rows_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Matches the token against the start of words in the row.
|
||||
* @param {string} token Token to match.
|
||||
* @param {number} maxMatches Max number of matches to return.
|
||||
* @param {!Array<?>} rows Rows to search for matches. Can be objects if they have
|
||||
* a toString method that returns the value to match against.
|
||||
* @return {!Array<?>} Rows that match.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.getPrefixMatchesForRows =
|
||||
function(token, maxMatches, rows) {
|
||||
var matches = [];
|
||||
|
||||
if (token != '') {
|
||||
var escapedToken = goog.string.regExpEscape(token);
|
||||
var matcher = new RegExp('(^|\\W+)' + escapedToken, 'i');
|
||||
|
||||
for (var i = 0; i < rows.length && matches.length < maxMatches; i++) {
|
||||
var row = rows[i];
|
||||
if (String(row).match(matcher)) {
|
||||
matches.push(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Matches the token against similar rows, by calculating "distance" between the
|
||||
* terms.
|
||||
* @param {string} token Token to match.
|
||||
* @param {number} maxMatches Max number of matches to return.
|
||||
* @return {!Array<?>} The best maxMatches rows.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.prototype.getSimilarRows = function(token, maxMatches) {
|
||||
return goog.ui.ac.ArrayMatcher.getSimilarMatchesForRows(token, maxMatches,
|
||||
this.rows_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Matches the token against similar rows, by calculating "distance" between the
|
||||
* terms.
|
||||
* @param {string} token Token to match.
|
||||
* @param {number} maxMatches Max number of matches to return.
|
||||
* @param {!Array<?>} rows Rows to search for matches. Can be objects
|
||||
* if they have a toString method that returns the value to
|
||||
* match against.
|
||||
* @return {!Array<?>} The best maxMatches rows.
|
||||
*/
|
||||
goog.ui.ac.ArrayMatcher.getSimilarMatchesForRows =
|
||||
function(token, maxMatches, rows) {
|
||||
var results = [];
|
||||
|
||||
for (var index = 0; index < rows.length; index++) {
|
||||
var row = rows[index];
|
||||
var str = token.toLowerCase();
|
||||
var txt = String(row).toLowerCase();
|
||||
var score = 0;
|
||||
|
||||
if (txt.indexOf(str) != -1) {
|
||||
score = parseInt((txt.indexOf(str) / 4).toString(), 10);
|
||||
|
||||
} else {
|
||||
var arr = str.split('');
|
||||
|
||||
var lastPos = -1;
|
||||
var penalty = 10;
|
||||
|
||||
for (var i = 0, c; c = arr[i]; i++) {
|
||||
var pos = txt.indexOf(c);
|
||||
|
||||
if (pos > lastPos) {
|
||||
var diff = pos - lastPos - 1;
|
||||
|
||||
if (diff > penalty - 5) {
|
||||
diff = penalty - 5;
|
||||
}
|
||||
|
||||
score += diff;
|
||||
|
||||
lastPos = pos;
|
||||
} else {
|
||||
score += penalty;
|
||||
penalty += 5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (score < str.length * 6) {
|
||||
results.push({
|
||||
str: row,
|
||||
score: score,
|
||||
index: index
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
results.sort(function(a, b) {
|
||||
var diff = a.score - b.score;
|
||||
if (diff != 0) {
|
||||
return diff;
|
||||
}
|
||||
return a.index - b.index;
|
||||
});
|
||||
|
||||
var matches = [];
|
||||
for (var i = 0; i < maxMatches && i < results.length; i++) {
|
||||
matches.push(results[i].str);
|
||||
}
|
||||
|
||||
return matches;
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2008 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.ArrayMatcher
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.ArrayMatcherTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2008 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.
|
||||
|
||||
goog.provide('goog.ui.ac.ArrayMatcherTest');
|
||||
goog.setTestOnly('goog.ui.ac.ArrayMatcherTest');
|
||||
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ac.ArrayMatcher');
|
||||
// TODO(arv): Add more useful tests for the similarity matching.
|
||||
|
||||
var ArrayMatcher = goog.ui.ac.ArrayMatcher;
|
||||
|
||||
function testRequestingRows() {
|
||||
var items = ['a', 'Ab', 'abc', 'ba', 'ca'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res;
|
||||
function matcher(token, matches) {
|
||||
assertEquals('a', token);
|
||||
res = matches;
|
||||
assertEquals('Should have three matches', 3, matches.length);
|
||||
assertEquals('a', matches[0]);
|
||||
assertEquals('Ab', matches[1]);
|
||||
assertEquals('abc', matches[2]);
|
||||
}
|
||||
|
||||
am.requestMatchingRows('a', 10, matcher);
|
||||
var res2 = goog.ui.ac.ArrayMatcher.getMatchesForRows('a', 10, items);
|
||||
assertArrayEquals(res, res2);
|
||||
}
|
||||
|
||||
function testRequestingRowsMaxMatches() {
|
||||
var items = ['a', 'Ab', 'abc', 'ba', 'ca'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
function matcher(token, matches) {
|
||||
assertEquals('a', token);
|
||||
assertEquals('Should have two matches', 2, matches.length);
|
||||
assertEquals('a', matches[0]);
|
||||
assertEquals('Ab', matches[1]);
|
||||
}
|
||||
|
||||
am.requestMatchingRows('a', 2, matcher);
|
||||
}
|
||||
|
||||
function testRequestingRowsSimilarMatches() {
|
||||
// No prefix matches so use similar
|
||||
var items = ['b', 'c', 'ba', 'ca'];
|
||||
var am = new ArrayMatcher(items, false);
|
||||
|
||||
function matcher(token, matches) {
|
||||
assertEquals('a', token);
|
||||
assertEquals('Should have two matches', 2, matches.length);
|
||||
assertEquals('ba', matches[0]);
|
||||
assertEquals('ca', matches[1]);
|
||||
}
|
||||
|
||||
am.requestMatchingRows('a', 10, matcher);
|
||||
}
|
||||
|
||||
function testRequestingRowsSimilarMatchesMaxMatches() {
|
||||
// No prefix matches so use similar
|
||||
var items = ['b', 'c', 'ba', 'ca'];
|
||||
var am = new ArrayMatcher(items, false);
|
||||
|
||||
function matcher(token, matches) {
|
||||
assertEquals('a', token);
|
||||
assertEquals('Should have one match', 1, matches.length);
|
||||
assertEquals('ba', matches[0]);
|
||||
}
|
||||
|
||||
am.requestMatchingRows('a', 1, matcher);
|
||||
}
|
||||
|
||||
function testGetPrefixMatches() {
|
||||
var items = ['a', 'b', 'c'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res = am.getPrefixMatches('a', 10);
|
||||
assertEquals('Should have one match', 1, res.length);
|
||||
assertEquals('Should return \'a\'', 'a', res[0]);
|
||||
var res2 = goog.ui.ac.ArrayMatcher.getPrefixMatchesForRows('a', 10, items);
|
||||
assertArrayEquals(res, res2);
|
||||
}
|
||||
|
||||
function testGetPrefixMatchesMaxMatches() {
|
||||
var items = ['a', 'Ab', 'abc', 'ba', 'ca'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res = am.getPrefixMatches('a', 2);
|
||||
assertEquals('Should have two matches', 2, res.length);
|
||||
assertEquals('a', res[0]);
|
||||
}
|
||||
|
||||
function testGetPrefixMatchesEmptyToken() {
|
||||
var items = ['a', 'b', 'c'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res = am.getPrefixMatches('', 10);
|
||||
assertEquals('Should have no matches', 0, res.length);
|
||||
}
|
||||
|
||||
function testGetSimilarRows() {
|
||||
var items = ['xa', 'xb', 'xc'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res = am.getSimilarRows('a', 10);
|
||||
assertEquals('Should have one match', 1, res.length);
|
||||
assertEquals('xa', res[0]);
|
||||
var res2 = goog.ui.ac.ArrayMatcher.getSimilarMatchesForRows('a', 10, items);
|
||||
assertArrayEquals(res, res2);
|
||||
}
|
||||
|
||||
function testGetSimilarRowsMaxMatches() {
|
||||
var items = ['xa', 'xAa', 'xaAa'];
|
||||
var am = new ArrayMatcher(items, true);
|
||||
|
||||
var res = am.getSimilarRows('a', 2);
|
||||
assertEquals('Should have two matches', 2, res.length);
|
||||
assertEquals('xa', res[0]);
|
||||
assertEquals('xAa', res[1]);
|
||||
}
|
||||
@@ -0,0 +1,921 @@
|
||||
// Copyright 2006 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 Gmail-like AutoComplete logic.
|
||||
*
|
||||
* @see ../../demos/autocomplete-basic.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.AutoComplete');
|
||||
goog.provide('goog.ui.ac.AutoComplete.EventType');
|
||||
|
||||
goog.require('goog.array');
|
||||
goog.require('goog.asserts');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.object');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* This is the central manager class for an AutoComplete instance. The matcher
|
||||
* can specify disabled rows that should not be hilited or selected by
|
||||
* implementing <code>isRowDisabled(row):boolean</code> for each autocomplete
|
||||
* row. No row will be considered disabled if this method is not implemented.
|
||||
*
|
||||
* @param {Object} matcher A data source and row matcher, implements
|
||||
* <code>requestMatchingRows(token, maxMatches, matchCallback)</code>.
|
||||
* @param {goog.events.EventTarget} renderer An object that implements
|
||||
* <code>
|
||||
* isVisible():boolean<br>
|
||||
* renderRows(rows:Array, token:string, target:Element);<br>
|
||||
* hiliteId(row-id:number);<br>
|
||||
* dismiss();<br>
|
||||
* dispose():
|
||||
* </code>.
|
||||
* @param {Object} selectionHandler An object that implements
|
||||
* <code>
|
||||
* selectRow(row);<br>
|
||||
* update(opt_force);
|
||||
* </code>.
|
||||
*
|
||||
* @constructor
|
||||
* @extends {goog.events.EventTarget}
|
||||
*/
|
||||
goog.ui.ac.AutoComplete = function(matcher, renderer, selectionHandler) {
|
||||
goog.events.EventTarget.call(this);
|
||||
|
||||
/**
|
||||
* A data-source which provides autocomplete suggestions.
|
||||
*
|
||||
* TODO(chrishenry): Tighten the type to !goog.ui.ac.AutoComplete.Matcher.
|
||||
*
|
||||
* @type {Object}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.matcher_ = matcher;
|
||||
|
||||
/**
|
||||
* A handler which interacts with the input DOM element (textfield, textarea,
|
||||
* or richedit).
|
||||
*
|
||||
* TODO(chrishenry): Tighten the type to !Object.
|
||||
*
|
||||
* @type {Object}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.selectionHandler_ = selectionHandler;
|
||||
|
||||
/**
|
||||
* A renderer to render/show/highlight/hide the autocomplete menu.
|
||||
* @type {goog.events.EventTarget}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.renderer_ = renderer;
|
||||
goog.events.listen(
|
||||
renderer,
|
||||
[
|
||||
goog.ui.ac.AutoComplete.EventType.HILITE,
|
||||
goog.ui.ac.AutoComplete.EventType.SELECT,
|
||||
goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS,
|
||||
goog.ui.ac.AutoComplete.EventType.DISMISS
|
||||
],
|
||||
this.handleEvent, false, this);
|
||||
|
||||
/**
|
||||
* Currently typed token which will be used for completion.
|
||||
* @type {?string}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.token_ = null;
|
||||
|
||||
/**
|
||||
* Autocomplete suggestion items.
|
||||
* @type {Array<?>}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.rows_ = [];
|
||||
|
||||
/**
|
||||
* Id of the currently highlighted row.
|
||||
* @type {number}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.hiliteId_ = -1;
|
||||
|
||||
/**
|
||||
* Id of the first row in autocomplete menu. Note that new ids are assigned
|
||||
* everytime new suggestions are fetched.
|
||||
*
|
||||
* TODO(chrishenry): Figure out what subclass does with this value
|
||||
* and whether we should expose a more proper API.
|
||||
*
|
||||
* @type {number}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.firstRowId_ = 0;
|
||||
|
||||
/**
|
||||
* The target HTML node for displaying.
|
||||
* @type {Element}
|
||||
* @protected
|
||||
* @suppress {underscore|visibility}
|
||||
*/
|
||||
this.target_ = null;
|
||||
|
||||
/**
|
||||
* The timer id for dismissing autocomplete menu with a delay.
|
||||
* @type {?number}
|
||||
* @private
|
||||
*/
|
||||
this.dismissTimer_ = null;
|
||||
|
||||
/**
|
||||
* Mapping from text input element to the anchor element. If the
|
||||
* mapping does not exist, the input element will act as the anchor
|
||||
* element.
|
||||
* @type {Object<Element>}
|
||||
* @private
|
||||
*/
|
||||
this.inputToAnchorMap_ = {};
|
||||
};
|
||||
goog.inherits(goog.ui.ac.AutoComplete, goog.events.EventTarget);
|
||||
|
||||
|
||||
/**
|
||||
* The maximum number of matches that should be returned
|
||||
* @type {number}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.maxMatches_ = 10;
|
||||
|
||||
|
||||
/**
|
||||
* True iff the first row should automatically be highlighted
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.autoHilite_ = true;
|
||||
|
||||
|
||||
/**
|
||||
* True iff the user can unhilight all rows by pressing the up arrow.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.allowFreeSelect_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* True iff item selection should wrap around from last to first. If
|
||||
* allowFreeSelect_ is on in conjunction, there is a step of free selection
|
||||
* before wrapping.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.wrap_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether completion from suggestion triggers fetching new suggestion.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.triggerSuggestionsOnUpdate_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Events associated with the autocomplete
|
||||
* @enum {string}
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.EventType = {
|
||||
|
||||
/** A row has been highlighted by the renderer */
|
||||
ROW_HILITE: 'rowhilite',
|
||||
|
||||
// Note: The events below are used for internal autocomplete events only and
|
||||
// should not be used in non-autocomplete code.
|
||||
|
||||
/** A row has been mouseovered and should be highlighted by the renderer. */
|
||||
HILITE: 'hilite',
|
||||
|
||||
/** A row has been selected by the renderer */
|
||||
SELECT: 'select',
|
||||
|
||||
/** A dismiss event has occurred */
|
||||
DISMISS: 'dismiss',
|
||||
|
||||
/** Event that cancels a dismiss event */
|
||||
CANCEL_DISMISS: 'canceldismiss',
|
||||
|
||||
/**
|
||||
* Field value was updated. A row field is included and is non-null when a
|
||||
* row has been selected. The value of the row typically includes fields:
|
||||
* contactData and formattedValue as well as a toString function (though none
|
||||
* of these fields are guaranteed to exist). The row field may be used to
|
||||
* return custom-type row data.
|
||||
*/
|
||||
UPDATE: 'update',
|
||||
|
||||
/**
|
||||
* The list of suggestions has been updated, usually because either the list
|
||||
* has opened, or because the user has typed another character and the
|
||||
* suggestions have been updated, or the user has dismissed the autocomplete.
|
||||
*/
|
||||
SUGGESTIONS_UPDATE: 'suggestionsupdate'
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* requestMatchingRows:(!Function|undefined),
|
||||
* isRowDisabled:(!Function|undefined)
|
||||
* }}
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.Matcher;
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Object} The data source providing the `autocomplete
|
||||
* suggestions.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getMatcher = function() {
|
||||
return goog.asserts.assert(this.matcher_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the data source providing the autocomplete suggestions.
|
||||
*
|
||||
* See constructor documentation for the interface.
|
||||
*
|
||||
* @param {!Object} matcher The matcher.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setMatcher = function(matcher) {
|
||||
this.matcher_ = matcher;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Object} The handler used to interact with the input DOM
|
||||
* element (textfield, textarea, or richedit), e.g. to update the
|
||||
* input DOM element with selected value.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getSelectionHandler = function() {
|
||||
return goog.asserts.assert(this.selectionHandler_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {goog.events.EventTarget} The renderer that
|
||||
* renders/shows/highlights/hides the autocomplete menu.
|
||||
* See constructor documentation for the expected renderer API.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getRenderer = function() {
|
||||
return this.renderer_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the renderer that renders/shows/highlights/hides the autocomplete
|
||||
* menu.
|
||||
*
|
||||
* See constructor documentation for the expected renderer API.
|
||||
*
|
||||
* @param {goog.events.EventTarget} renderer The renderer.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setRenderer = function(renderer) {
|
||||
this.renderer_ = renderer;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {?string} The currently typed token used for completion.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getToken = function() {
|
||||
return this.token_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the current token (without changing the rendered autocompletion).
|
||||
*
|
||||
* NOTE(chrishenry): This method will likely go away when we figure
|
||||
* out a better API.
|
||||
*
|
||||
* @param {?string} token The new token.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setTokenInternal = function(token) {
|
||||
this.token_ = token;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {number} index The suggestion index, must be within the
|
||||
* interval [0, this.getSuggestionCount()).
|
||||
* @return {Object} The currently suggested item at the given index
|
||||
* (or null if there is none).
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getSuggestion = function(index) {
|
||||
return this.rows_[index];
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {!Array<?>} The current autocomplete suggestion items.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getAllSuggestions = function() {
|
||||
return goog.asserts.assert(this.rows_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The number of currently suggested items.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getSuggestionCount = function() {
|
||||
return this.rows_.length;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} The id (not index!) of the currently highlighted row.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getHighlightedId = function() {
|
||||
return this.hiliteId_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Generic event handler that handles any events this object is listening to.
|
||||
* @param {goog.events.Event} e Event Object.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.handleEvent = function(e) {
|
||||
var matcher = /** @type {?goog.ui.ac.AutoComplete.Matcher} */ (this.matcher_);
|
||||
|
||||
if (e.target == this.renderer_) {
|
||||
switch (e.type) {
|
||||
case goog.ui.ac.AutoComplete.EventType.HILITE:
|
||||
this.hiliteId(/** @type {number} */ (e.row));
|
||||
break;
|
||||
|
||||
case goog.ui.ac.AutoComplete.EventType.SELECT:
|
||||
var rowDisabled = false;
|
||||
|
||||
// e.row can be either a valid row id or empty.
|
||||
if (goog.isNumber(e.row)) {
|
||||
var rowId = e.row;
|
||||
var index = this.getIndexOfId(rowId);
|
||||
var row = this.rows_[index];
|
||||
|
||||
// Make sure the row selected is not a disabled row.
|
||||
rowDisabled = !!row && matcher.isRowDisabled &&
|
||||
matcher.isRowDisabled(row);
|
||||
if (row && !rowDisabled && this.hiliteId_ != rowId) {
|
||||
// Event target row not currently highlighted - fix the mismatch.
|
||||
this.hiliteId(rowId);
|
||||
}
|
||||
}
|
||||
if (!rowDisabled) {
|
||||
// Note that rowDisabled can be false even if e.row does not
|
||||
// contain a valid row ID; at least one client depends on us
|
||||
// proceeding anyway.
|
||||
this.selectHilited();
|
||||
}
|
||||
break;
|
||||
|
||||
case goog.ui.ac.AutoComplete.EventType.CANCEL_DISMISS:
|
||||
this.cancelDelayedDismiss();
|
||||
break;
|
||||
|
||||
case goog.ui.ac.AutoComplete.EventType.DISMISS:
|
||||
this.dismissOnDelay();
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the max number of matches to fetch from the Matcher.
|
||||
*
|
||||
* @param {number} max Max number of matches.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setMaxMatches = function(max) {
|
||||
this.maxMatches_ = max;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether or not the first row should be highlighted by default.
|
||||
*
|
||||
* @param {boolean} autoHilite true iff the first row should be
|
||||
* highlighted by default.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setAutoHilite = function(autoHilite) {
|
||||
this.autoHilite_ = autoHilite;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether or not the up/down arrow can unhilite all rows.
|
||||
*
|
||||
* @param {boolean} allowFreeSelect true iff the up arrow can unhilite all rows.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setAllowFreeSelect =
|
||||
function(allowFreeSelect) {
|
||||
this.allowFreeSelect_ = allowFreeSelect;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether or not selections can wrap around the edges.
|
||||
*
|
||||
* @param {boolean} wrap true iff sections should wrap around the edges.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setWrap = function(wrap) {
|
||||
this.wrap_ = wrap;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets whether or not to request new suggestions immediately after completion
|
||||
* of a suggestion.
|
||||
*
|
||||
* @param {boolean} triggerSuggestionsOnUpdate true iff completion should fetch
|
||||
* new suggestions.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setTriggerSuggestionsOnUpdate = function(
|
||||
triggerSuggestionsOnUpdate) {
|
||||
this.triggerSuggestionsOnUpdate_ = triggerSuggestionsOnUpdate;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the token to match against. This triggers calls to the Matcher to
|
||||
* fetch the matches (up to maxMatches), and then it triggers a call to
|
||||
* <code>renderer.renderRows()</code>.
|
||||
*
|
||||
* @param {string} token The string for which to search in the Matcher.
|
||||
* @param {string=} opt_fullString Optionally, the full string in the input
|
||||
* field.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setToken = function(token, opt_fullString) {
|
||||
if (this.token_ == token) {
|
||||
return;
|
||||
}
|
||||
this.token_ = token;
|
||||
this.matcher_.requestMatchingRows(this.token_,
|
||||
this.maxMatches_, goog.bind(this.matchListener_, this), opt_fullString);
|
||||
this.cancelDelayedDismiss();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the current target HTML node for displaying autocomplete UI.
|
||||
* @return {Element} The current target HTML node for displaying autocomplete
|
||||
* UI.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getTarget = function() {
|
||||
return this.target_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Sets the current target HTML node for displaying autocomplete UI.
|
||||
* Can be an implementation specific definition of how to display UI in relation
|
||||
* to the target node.
|
||||
* This target will be passed into <code>renderer.renderRows()</code>
|
||||
*
|
||||
* @param {Element} target The current target HTML node for displaying
|
||||
* autocomplete UI.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.setTarget = function(target) {
|
||||
this.target_ = target;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} Whether the autocomplete's renderer is open.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.isOpen = function() {
|
||||
return this.renderer_.isVisible();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {number} Number of rows in the autocomplete.
|
||||
* @deprecated Use this.getSuggestionCount().
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getRowCount = function() {
|
||||
return this.getSuggestionCount();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Moves the hilite to the next non-disabled row.
|
||||
* Calls renderer.hiliteId() when there's something to do.
|
||||
* @return {boolean} Returns true on a successful hilite.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.hiliteNext = function() {
|
||||
var lastId = this.firstRowId_ + this.rows_.length - 1;
|
||||
var toHilite = this.hiliteId_;
|
||||
// Hilite the next row, skipping any disabled rows.
|
||||
for (var i = 0; i < this.rows_.length; i++) {
|
||||
// Increment to the next row.
|
||||
if (toHilite >= this.firstRowId_ && toHilite < lastId) {
|
||||
toHilite++;
|
||||
} else if (toHilite == -1) {
|
||||
toHilite = this.firstRowId_;
|
||||
} else if (this.allowFreeSelect_ && toHilite == lastId) {
|
||||
this.hiliteId(-1);
|
||||
return false;
|
||||
} else if (this.wrap_ && toHilite == lastId) {
|
||||
toHilite = this.firstRowId_;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.hiliteId(toHilite)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Moves the hilite to the previous non-disabled row. Calls
|
||||
* renderer.hiliteId() when there's something to do.
|
||||
* @return {boolean} Returns true on a successful hilite.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.hilitePrev = function() {
|
||||
var lastId = this.firstRowId_ + this.rows_.length - 1;
|
||||
var toHilite = this.hiliteId_;
|
||||
// Hilite the previous row, skipping any disabled rows.
|
||||
for (var i = 0; i < this.rows_.length; i++) {
|
||||
// Decrement to the previous row.
|
||||
if (toHilite > this.firstRowId_) {
|
||||
toHilite--;
|
||||
} else if (this.allowFreeSelect_ && toHilite == this.firstRowId_) {
|
||||
this.hiliteId(-1);
|
||||
return false;
|
||||
} else if (this.wrap_ && (toHilite == -1 || toHilite == this.firstRowId_)) {
|
||||
toHilite = lastId;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.hiliteId(toHilite)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Hilites the id if it's valid and the row is not disabled, otherwise does
|
||||
* nothing.
|
||||
* @param {number} id A row id (not index).
|
||||
* @return {boolean} Whether the id was hilited. Returns false if the row is
|
||||
* disabled.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.hiliteId = function(id) {
|
||||
var index = this.getIndexOfId(id);
|
||||
var row = this.rows_[index];
|
||||
var rowDisabled = !!row && this.matcher_.isRowDisabled &&
|
||||
this.matcher_.isRowDisabled(row);
|
||||
if (!rowDisabled) {
|
||||
this.hiliteId_ = id;
|
||||
this.renderer_.hiliteId(id);
|
||||
return index != -1;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Hilites the index, if it's valid and the row is not disabled, otherwise does
|
||||
* nothing.
|
||||
* @param {number} index The row's index.
|
||||
* @return {boolean} Whether the index was hilited.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.hiliteIndex = function(index) {
|
||||
return this.hiliteId(this.getIdOfIndex_(index));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* If there are any current matches, this passes the hilited row data to
|
||||
* <code>selectionHandler.selectRow()</code>
|
||||
* @return {boolean} Whether there are any current matches.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.selectHilited = function() {
|
||||
var index = this.getIndexOfId(this.hiliteId_);
|
||||
if (index != -1) {
|
||||
var selectedRow = this.rows_[index];
|
||||
var suppressUpdate = this.selectionHandler_.selectRow(selectedRow);
|
||||
if (this.triggerSuggestionsOnUpdate_) {
|
||||
this.token_ = null;
|
||||
this.dismissOnDelay();
|
||||
} else {
|
||||
this.dismiss();
|
||||
}
|
||||
if (!suppressUpdate) {
|
||||
this.dispatchEvent({
|
||||
type: goog.ui.ac.AutoComplete.EventType.UPDATE,
|
||||
row: selectedRow,
|
||||
index: index
|
||||
});
|
||||
if (this.triggerSuggestionsOnUpdate_) {
|
||||
this.selectionHandler_.update(true);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} else {
|
||||
this.dismiss();
|
||||
this.dispatchEvent(
|
||||
{
|
||||
type: goog.ui.ac.AutoComplete.EventType.UPDATE,
|
||||
row: null,
|
||||
index: null
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether or not the autocomplete is open and has a highlighted row.
|
||||
* @return {boolean} Whether an autocomplete row is highlighted.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.hasHighlight = function() {
|
||||
return this.isOpen() && this.getIndexOfId(this.hiliteId_) != -1;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Clears out the token, rows, and hilite, and calls
|
||||
* <code>renderer.dismiss()</code>
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.dismiss = function() {
|
||||
this.hiliteId_ = -1;
|
||||
this.token_ = null;
|
||||
this.firstRowId_ += this.rows_.length;
|
||||
this.rows_ = [];
|
||||
window.clearTimeout(this.dismissTimer_);
|
||||
this.dismissTimer_ = null;
|
||||
this.renderer_.dismiss();
|
||||
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
|
||||
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.DISMISS);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Call a dismiss after a delay, if there's already a dismiss active, ignore.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.dismissOnDelay = function() {
|
||||
if (!this.dismissTimer_) {
|
||||
this.dismissTimer_ = window.setTimeout(goog.bind(this.dismiss, this), 100);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cancels any delayed dismiss events immediately.
|
||||
* @return {boolean} Whether a delayed dismiss was cancelled.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.immediatelyCancelDelayedDismiss_ =
|
||||
function() {
|
||||
if (this.dismissTimer_) {
|
||||
window.clearTimeout(this.dismissTimer_);
|
||||
this.dismissTimer_ = null;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Cancel the active delayed dismiss if there is one.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.cancelDelayedDismiss = function() {
|
||||
// Under certain circumstances a cancel event occurs immediately prior to a
|
||||
// delayedDismiss event that it should be cancelling. To handle this situation
|
||||
// properly, a timer is used to stop that event.
|
||||
// Using only the timer creates undesirable behavior when the cancel occurs
|
||||
// less than 10ms before the delayed dismiss timout ends. If that happens the
|
||||
// clearTimeout() will occur too late and have no effect.
|
||||
if (!this.immediatelyCancelDelayedDismiss_()) {
|
||||
window.setTimeout(goog.bind(this.immediatelyCancelDelayedDismiss_, this),
|
||||
10);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ac.AutoComplete.prototype.disposeInternal = function() {
|
||||
goog.ui.ac.AutoComplete.superClass_.disposeInternal.call(this);
|
||||
delete this.inputToAnchorMap_;
|
||||
this.renderer_.dispose();
|
||||
this.selectionHandler_.dispose();
|
||||
this.matcher_ = null;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Callback passed to Matcher when requesting matches for a token.
|
||||
* This might be called synchronously, or asynchronously, or both, for
|
||||
* any implementation of a Matcher.
|
||||
* If the Matcher calls this back, with the same token this AutoComplete
|
||||
* has set currently, then this will package the matching rows in object
|
||||
* of the form
|
||||
* <pre>
|
||||
* {
|
||||
* id: an integer ID unique to this result set and AutoComplete instance,
|
||||
* data: the raw row data from Matcher
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param {string} matchedToken Token that corresponds with the rows.
|
||||
* @param {!Array<?>} rows Set of data that match the given token.
|
||||
* @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
|
||||
* keeps the currently hilited (by index) element hilited. If false not.
|
||||
* Otherwise a RenderOptions object.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.matchListener_ =
|
||||
function(matchedToken, rows, opt_options) {
|
||||
if (this.token_ != matchedToken) {
|
||||
// Matcher's response token doesn't match current token.
|
||||
// This is probably an async response that came in after
|
||||
// the token was changed, so don't do anything.
|
||||
return;
|
||||
}
|
||||
|
||||
this.renderRows(rows, opt_options);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Renders the rows and adds highlighting.
|
||||
* @param {!Array<?>} rows Set of data that match the given token.
|
||||
* @param {(boolean|goog.ui.ac.RenderOptions)=} opt_options If true,
|
||||
* keeps the currently hilited (by index) element hilited. If false not.
|
||||
* Otherwise a RenderOptions object.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.renderRows = function(rows, opt_options) {
|
||||
// The optional argument should be a RenderOptions object. It can be a
|
||||
// boolean for backwards compatibility, defaulting to false.
|
||||
var optionsObj = goog.typeOf(opt_options) == 'object' && opt_options;
|
||||
|
||||
var preserveHilited =
|
||||
optionsObj ? optionsObj.getPreserveHilited() : opt_options;
|
||||
var indexToHilite = preserveHilited ? this.getIndexOfId(this.hiliteId_) : -1;
|
||||
|
||||
// Current token matches the matcher's response token.
|
||||
this.firstRowId_ += this.rows_.length;
|
||||
this.rows_ = rows;
|
||||
var rendRows = [];
|
||||
for (var i = 0; i < rows.length; ++i) {
|
||||
rendRows.push({
|
||||
id: this.getIdOfIndex_(i),
|
||||
data: rows[i]
|
||||
});
|
||||
}
|
||||
|
||||
var anchor = null;
|
||||
if (this.target_) {
|
||||
anchor = this.inputToAnchorMap_[goog.getUid(this.target_)] || this.target_;
|
||||
}
|
||||
this.renderer_.setAnchorElement(anchor);
|
||||
this.renderer_.renderRows(rendRows, this.token_, this.target_);
|
||||
|
||||
var autoHilite = this.autoHilite_;
|
||||
if (optionsObj && optionsObj.getAutoHilite() !== undefined) {
|
||||
autoHilite = optionsObj.getAutoHilite();
|
||||
}
|
||||
this.hiliteId_ = -1;
|
||||
if ((autoHilite || indexToHilite >= 0) &&
|
||||
rendRows.length != 0 &&
|
||||
this.token_) {
|
||||
if (indexToHilite >= 0) {
|
||||
this.hiliteId(this.getIdOfIndex_(indexToHilite));
|
||||
} else {
|
||||
// Hilite the first non-disabled row.
|
||||
this.hiliteNext();
|
||||
}
|
||||
}
|
||||
this.dispatchEvent(goog.ui.ac.AutoComplete.EventType.SUGGESTIONS_UPDATE);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the index corresponding to a particular id.
|
||||
* @param {number} id A unique id for the row.
|
||||
* @return {number} A valid index into rows_, or -1 if the id is invalid.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getIndexOfId = function(id) {
|
||||
var index = id - this.firstRowId_;
|
||||
if (index < 0 || index >= this.rows_.length) {
|
||||
return -1;
|
||||
}
|
||||
return index;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the id corresponding to a particular index. (Does no checking.)
|
||||
* @param {number} index The index of a row in the result set.
|
||||
* @return {number} The id that currently corresponds to that index.
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.getIdOfIndex_ = function(index) {
|
||||
return this.firstRowId_ + index;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attach text areas or input boxes to the autocomplete by DOM reference. After
|
||||
* elements are attached to the autocomplete, when a user types they will see
|
||||
* the autocomplete drop down.
|
||||
* @param {...Element} var_args Variable args: Input or text area elements to
|
||||
* attach the autocomplete too.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.attachInputs = function(var_args) {
|
||||
// Delegate to the input handler
|
||||
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
|
||||
(this.selectionHandler_);
|
||||
inputHandler.attachInputs.apply(inputHandler, arguments);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Detach text areas or input boxes to the autocomplete by DOM reference.
|
||||
* @param {...Element} var_args Variable args: Input or text area elements to
|
||||
* detach from the autocomplete.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.detachInputs = function(var_args) {
|
||||
// Delegate to the input handler
|
||||
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
|
||||
(this.selectionHandler_);
|
||||
inputHandler.detachInputs.apply(inputHandler, arguments);
|
||||
|
||||
// Remove mapping from input to anchor if one exists.
|
||||
goog.array.forEach(arguments, function(input) {
|
||||
goog.object.remove(this.inputToAnchorMap_, goog.getUid(input));
|
||||
}, this);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Attaches the autocompleter to a text area or text input element
|
||||
* with an anchor element. The anchor element is the element the
|
||||
* autocomplete box will be positioned against.
|
||||
* @param {Element} inputElement The input element. May be 'textarea',
|
||||
* text 'input' element, or any other element that exposes similar
|
||||
* interface.
|
||||
* @param {Element} anchorElement The anchor element.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.attachInputWithAnchor = function(
|
||||
inputElement, anchorElement) {
|
||||
this.inputToAnchorMap_[goog.getUid(inputElement)] = anchorElement;
|
||||
this.attachInputs(inputElement);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Forces an update of the display.
|
||||
* @param {boolean=} opt_force Whether to force an update.
|
||||
*/
|
||||
goog.ui.ac.AutoComplete.prototype.update = function(opt_force) {
|
||||
var inputHandler = /** @type {goog.ui.ac.InputHandler} */
|
||||
(this.selectionHandler_);
|
||||
inputHandler.update(opt_force);
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2006 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<!--
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.AutoComplete
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.AutoCompleteTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="test-area">
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,273 @@
|
||||
// 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;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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.rows_ = [];
|
||||
this.rowStrings_ = {};
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* 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_();
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2013 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.ArrayMatcher
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.CachingMatcherTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,214 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.ui.ac.CachingMatcherTest');
|
||||
goog.setTestOnly('goog.ui.ac.CachingMatcherTest');
|
||||
|
||||
goog.require('goog.testing.MockControl');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.mockmatchers');
|
||||
goog.require('goog.ui.ac.CachingMatcher');
|
||||
ignoreArgument = goog.testing.mockmatchers.ignoreArgument;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Fake version of Throttle which only fires when we call permitOne().
|
||||
* @constructor
|
||||
* @suppress {missingProvide}
|
||||
*/
|
||||
goog.async.Throttle = function(fn, time, self) {
|
||||
this.fn = fn;
|
||||
this.time = time;
|
||||
this.self = self;
|
||||
this.numFires = 0;
|
||||
};
|
||||
|
||||
|
||||
/** @suppress {missingProvide} */
|
||||
goog.async.Throttle.prototype.fire = function() {
|
||||
this.numFires++;
|
||||
};
|
||||
|
||||
|
||||
/** @suppress {missingProvide} */
|
||||
goog.async.Throttle.prototype.permitOne = function() {
|
||||
if (this.numFires == 0) {
|
||||
return;
|
||||
}
|
||||
this.fn.call(this.self);
|
||||
this.numFires = 0;
|
||||
};
|
||||
|
||||
// Actual tests.
|
||||
var mockControl;
|
||||
var mockMatcher;
|
||||
var mockHandler;
|
||||
var matcher;
|
||||
|
||||
function setUp() {
|
||||
mockControl = new goog.testing.MockControl();
|
||||
mockMatcher = {
|
||||
requestMatchingRows: mockControl.createFunctionMock('requestMatchingRows')
|
||||
};
|
||||
mockHandler = mockControl.createFunctionMock('matchHandler');
|
||||
matcher = new goog.ui.ac.CachingMatcher(mockMatcher);
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
mockControl.$tearDown();
|
||||
}
|
||||
|
||||
function testLocalThenRemoteMatch() {
|
||||
// We immediately get the local match.
|
||||
mockHandler('foo', []);
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('foo', 12, mockHandler);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// Now we run the remote match.
|
||||
mockHandler('foo', ['foo1', 'foo2'], ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('foo', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('foo', ['foo1', 'foo2', 'bar3']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
|
||||
function testCacheSize() {
|
||||
matcher.setMaxCacheSize(4);
|
||||
|
||||
// First we populate, but not overflow the cache.
|
||||
mockHandler('foo', []);
|
||||
mockHandler('foo', ['foo111', 'foo222'], ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('foo', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('foo', ['foo111', 'foo222', 'bar333']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('foo', 12, mockHandler);
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// Now we verify the cache is populated.
|
||||
mockHandler('foo1', ['foo111']);
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('foo1', 12, mockHandler);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// Now we overflow the cache. Check that the remote results show the first
|
||||
// time we get them back, even though they overflow the cache.
|
||||
mockHandler('foo11', ['foo111']);
|
||||
mockHandler('foo11', ['foo111', 'foo112', 'foo113', 'foo114'],
|
||||
ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('foo11', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('foo11', ['foo111', 'foo112', 'foo113', 'foo114']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('foo11', 12, mockHandler);
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// Now check that the cache is empty.
|
||||
mockHandler('foo11', []);
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('foo11', 12, mockHandler);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
|
||||
function testSimilarMatchingDoesntReorderResults() {
|
||||
// Populate the cache. We get two prefix matches.
|
||||
mockHandler('ba', []);
|
||||
mockHandler('ba', ['bar', 'baz', 'bam'], ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('ba', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('ba', ['bar', 'baz', 'bam']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('ba', 12, mockHandler);
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// The user types another character. The local match gives us two similar
|
||||
// matches, but no prefix matches. The remote match returns a prefix match,
|
||||
// which would normally be ranked above the similar matches, but gets ranked
|
||||
// below the similar matches because the user hasn't typed any more
|
||||
// characters.
|
||||
mockHandler('bad', ['bar', 'baz', 'bam']);
|
||||
mockHandler('bad', ['bar', 'baz', 'bam', 'bad', 'badder', 'baddest'],
|
||||
ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('bad', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('bad', ['bad', 'badder', 'baddest']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('bad', 12, mockHandler);
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
|
||||
// The user types yet another character, which allows the prefix matches to
|
||||
// jump to the top of the list of suggestions.
|
||||
mockHandler('badd', ['badder', 'baddest']);
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows('badd', 12, mockHandler);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
|
||||
function testSetThrottleTime() {
|
||||
assertEquals(150, matcher.throttledTriggerBaseMatch_.time);
|
||||
matcher.setThrottleTime(234);
|
||||
assertEquals(234, matcher.throttledTriggerBaseMatch_.time);
|
||||
}
|
||||
|
||||
function testSetBaseMatcherMaxMatches() {
|
||||
mockHandler('foo', []); // Local match
|
||||
mockMatcher.requestMatchingRows('foo', 789, ignoreArgument);
|
||||
mockControl.$replayAll();
|
||||
matcher.setBaseMatcherMaxMatches();
|
||||
matcher.requestMatchingRows('foo', 12, mockHandler);
|
||||
}
|
||||
|
||||
function testSetLocalMatcher() {
|
||||
// Use a local matcher which just sorts all the rows alphabetically.
|
||||
function sillyMatcher(token, maxMatches, rows) {
|
||||
rows = rows.concat([]);
|
||||
rows.sort();
|
||||
return rows;
|
||||
}
|
||||
|
||||
mockHandler('foo', []);
|
||||
mockHandler('foo', ['a', 'b', 'c'], ignoreArgument);
|
||||
mockMatcher.requestMatchingRows('foo', 100, ignoreArgument)
|
||||
.$does(function(token, maxResults, matchHandler) {
|
||||
matchHandler('foo', ['b', 'a', 'c']);
|
||||
});
|
||||
mockControl.$replayAll();
|
||||
matcher.setLocalMatcher(sillyMatcher);
|
||||
matcher.requestMatchingRows('foo', 12, mockHandler);
|
||||
matcher.throttledTriggerBaseMatch_.permitOne();
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2007 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<!--
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.InputHandler
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.InputHandlerTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<input type="text" id="textInput" style="display:none" />
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,716 @@
|
||||
// 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.
|
||||
|
||||
goog.provide('goog.ui.ac.InputHandlerTest');
|
||||
goog.setTestOnly('goog.ui.ac.InputHandlerTest');
|
||||
|
||||
goog.require('goog.dom.selection');
|
||||
goog.require('goog.events.BrowserEvent');
|
||||
goog.require('goog.events.Event');
|
||||
goog.require('goog.events.EventTarget');
|
||||
goog.require('goog.events.KeyCodes');
|
||||
goog.require('goog.functions');
|
||||
goog.require('goog.object');
|
||||
goog.require('goog.testing.MockClock');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.ui.ac.InputHandler');
|
||||
goog.require('goog.userAgent');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Mock out the input element.
|
||||
* @constructor
|
||||
*/
|
||||
function MockElement() {
|
||||
goog.events.EventTarget.call(this);
|
||||
this.setAttributeNS = function() {};
|
||||
this.setAttribute = function(key, value) { this[key] = value; };
|
||||
this.focus = function() {};
|
||||
this.blur = function() {};
|
||||
this.ownerDocument = document;
|
||||
this.selectionStart = 0;
|
||||
}
|
||||
goog.inherits(MockElement, goog.events.EventTarget);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @constructor
|
||||
*/
|
||||
function MockAutoCompleter() {
|
||||
this.setToken = null;
|
||||
this.setTokenWasCalled = false;
|
||||
this.selectHilitedWasCalled = false;
|
||||
this.dismissWasCalled = false;
|
||||
this.getTarget = function() { return mockElement };
|
||||
this.setTarget = function() { };
|
||||
this.setToken = function(token) {
|
||||
this.setTokenWasCalled = true;
|
||||
this.setToken = token;
|
||||
};
|
||||
this.selectHilited = function() {
|
||||
this.selectHilitedWasCalled = true;
|
||||
return true; // Success.
|
||||
};
|
||||
this.cancelDelayedDismiss = function() { };
|
||||
this.dismissOnDelay = function() {};
|
||||
this.dismiss = function() { this.dismissWasCalled = true; };
|
||||
this.isOpen = goog.functions.TRUE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* MockInputHandler simulates key events for testing the IME behavior of
|
||||
* InputHandler.
|
||||
* @constructor
|
||||
*/
|
||||
function MockInputHandler() {
|
||||
goog.ui.ac.InputHandler.call(this);
|
||||
|
||||
this.ac_ = new MockAutoCompleter();
|
||||
this.cursorPosition_ = 0;
|
||||
|
||||
this.attachInput(mockElement);
|
||||
}
|
||||
goog.inherits(MockInputHandler, goog.ui.ac.InputHandler);
|
||||
|
||||
|
||||
/** Checks for updates to the text area, should not happen during IME. */
|
||||
MockInputHandler.prototype.update = function() {
|
||||
this.updates++;
|
||||
};
|
||||
|
||||
|
||||
/** Simulates key events. */
|
||||
MockInputHandler.prototype.fireKeyEvents = function(
|
||||
keyCode, down, press, up, opt_properties) {
|
||||
if (down) this.fireEvent('keydown', keyCode, opt_properties);
|
||||
if (press) this.fireEvent('keypress', keyCode, opt_properties);
|
||||
if (up) this.fireEvent('keyup', keyCode, opt_properties);
|
||||
};
|
||||
|
||||
|
||||
/** Simulates an event. */
|
||||
MockInputHandler.prototype.fireEvent = function(
|
||||
type, keyCode, opt_properties) {
|
||||
var e = {};
|
||||
e.type = type;
|
||||
e.keyCode = keyCode;
|
||||
e.preventDefault = function() {};
|
||||
if (!goog.userAgent.IE) {
|
||||
e.which = type == 'keydown' ? keyCode : 0;
|
||||
}
|
||||
if (opt_properties) {
|
||||
goog.object.extend(e, opt_properties);
|
||||
}
|
||||
e = new goog.events.BrowserEvent(e);
|
||||
mockElement.dispatchEvent(e);
|
||||
};
|
||||
|
||||
MockInputHandler.prototype.setCursorPosition = function(cursorPosition) {
|
||||
this.cursorPosition_ = cursorPosition;
|
||||
};
|
||||
|
||||
MockInputHandler.prototype.getCursorPosition = function() {
|
||||
return this.cursorPosition_;
|
||||
};
|
||||
|
||||
// Variables used by all test
|
||||
var mh = null;
|
||||
var oldMac, oldWin, oldLinux, oldIe, oldFf, oldWebkit, oldVersion;
|
||||
var oldUsesKeyDown;
|
||||
var mockElement;
|
||||
var mockClock;
|
||||
|
||||
function setUp() {
|
||||
oldMac = goog.userAgent.MAC;
|
||||
oldWin = goog.userAgent.WINDOWS;
|
||||
oldLinux = goog.userAgent.LINUX;
|
||||
oldIe = goog.userAgent.IE;
|
||||
oldFf = goog.userAgent.GECKO;
|
||||
oldWebkit = goog.userAgent.WEBKIT;
|
||||
oldVersion = goog.userAgent.VERSION;
|
||||
oldUsesKeyDown = goog.events.KeyHandler.USES_KEYDOWN_;
|
||||
mockClock = new goog.testing.MockClock(true);
|
||||
mockElement = new MockElement;
|
||||
mh = new MockInputHandler;
|
||||
}
|
||||
|
||||
function tearDown() {
|
||||
goog.userAgent.MAC = oldMac;
|
||||
goog.userAgent.WINDOWS = oldWin;
|
||||
goog.userAgent.LINUX = oldLinux;
|
||||
goog.userAgent.IE = oldIe;
|
||||
goog.userAgent.GECKO = oldFf;
|
||||
goog.userAgent.WEBKIT = oldWebkit;
|
||||
goog.userAgent.VERSION = oldVersion;
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = oldUsesKeyDown;
|
||||
mockClock.dispose();
|
||||
mockElement.dispose();
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Windows/Firefox3 */
|
||||
function simulateWinFirefox3() {
|
||||
goog.userAgent.MAC = false;
|
||||
goog.userAgent.WINDOWS = true;
|
||||
goog.userAgent.LINUX = false;
|
||||
goog.userAgent.IE = false;
|
||||
goog.userAgent.GECKO = true;
|
||||
goog.userAgent.WEBKIT = false;
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = false;
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Windows/InternetExplorer7 */
|
||||
function simulateWinIe7() {
|
||||
goog.userAgent.MAC = false;
|
||||
goog.userAgent.WINDOWS = true;
|
||||
goog.userAgent.LINUX = false;
|
||||
goog.userAgent.IE = true;
|
||||
goog.userAgent.DOCUMENT_MODE = 7;
|
||||
goog.userAgent.GECKO = false;
|
||||
goog.userAgent.WEBKIT = false;
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = true;
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Windows/Chrome */
|
||||
function simulateWinChrome() {
|
||||
goog.userAgent.MAC = false;
|
||||
goog.userAgent.WINDOWS = true;
|
||||
goog.userAgent.LINUX = false;
|
||||
goog.userAgent.IE = false;
|
||||
goog.userAgent.GECKO = false;
|
||||
goog.userAgent.WEBKIT = true;
|
||||
goog.userAgent.VERSION = '525';
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = true;
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Mac/Firefox3 */
|
||||
function simulateMacFirefox3() {
|
||||
goog.userAgent.MAC = true;
|
||||
goog.userAgent.WINDOWS = false;
|
||||
goog.userAgent.LINUX = false;
|
||||
goog.userAgent.IE = false;
|
||||
goog.userAgent.GECKO = true;
|
||||
goog.userAgent.WEBKIT = false;
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = true;
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Mac/Safari3 */
|
||||
function simulateMacSafari3() {
|
||||
goog.userAgent.MAC = true;
|
||||
goog.userAgent.WINDOWS = false;
|
||||
goog.userAgent.LINUX = false;
|
||||
goog.userAgent.IE = false;
|
||||
goog.userAgent.GECKO = false;
|
||||
goog.userAgent.WEBKIT = true;
|
||||
goog.userAgent.VERSION = '525';
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = true;
|
||||
}
|
||||
|
||||
|
||||
/** Used to simulate behavior of Linux/Firefox3 */
|
||||
function simulateLinuxFirefox3() {
|
||||
goog.userAgent.MAC = false;
|
||||
goog.userAgent.WINDOWS = false;
|
||||
goog.userAgent.LINUX = true;
|
||||
goog.userAgent.IE = false;
|
||||
goog.userAgent.GECKO = true;
|
||||
goog.userAgent.WEBKIT = false;
|
||||
goog.events.KeyHandler.USES_KEYDOWN_ = true;
|
||||
}
|
||||
|
||||
|
||||
/** Test the normal, non-IME case */
|
||||
function testRegularKey() {
|
||||
// Each key fires down, press, and up in that order, and each should
|
||||
// trigger an autocomplete update
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.K, true, true, true);
|
||||
assertFalse('IME should not be triggered by K', mh.waitingForIme_);
|
||||
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, true, true, true);
|
||||
assertFalse('IME should not be triggered by A', mh.waitingForIme_);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Windows/Firefox3.
|
||||
*/
|
||||
function testImeWinFirefox3() {
|
||||
simulateWinFirefox3();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
// Event is not generated for key code a.
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
// Event is not generated for key code i.
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Windows/InternetExplorer7.
|
||||
*/
|
||||
function testImeWinIe7() {
|
||||
simulateWinIe7();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Windows/Chrome.
|
||||
*/
|
||||
function testImeWinChrome() {
|
||||
simulateWinChrome();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Mac/Firefox3.
|
||||
*/
|
||||
function testImeMacFirefox3() {
|
||||
// TODO(user): Currently our code cannot distinguish preedit characters
|
||||
// from normal ones for Mac/Firefox3.
|
||||
// Enable this test after we fix it.
|
||||
|
||||
simulateMacFirefox3();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, true, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.I, true, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Mac/Safari3.
|
||||
*/
|
||||
function testImeMacSafari3() {
|
||||
simulateMacSafari3();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, false, false, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, false, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.I, false, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* This test simulates the key inputs generated by pressing
|
||||
* '<ime_on>a<enter>i<ime_off>u' using the Japanese IME
|
||||
* on Linux/Firefox3.
|
||||
*/
|
||||
function testImeLinuxFirefox3() {
|
||||
// TODO(user): Currently our code cannot distinguish preedit characters
|
||||
// from normal ones for Linux/Firefox3.
|
||||
// Enable this test after we fix it.
|
||||
|
||||
|
||||
simulateLinuxFirefox3();
|
||||
mh.fireEvent('focus', '');
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_on
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
|
||||
// a
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.A, true, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// enter
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
// i
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.WIN_IME, true, true, false);
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.I, true, false, true);
|
||||
assertTrue('IME should be triggered', mh.waitingForIme_);
|
||||
|
||||
// ime_off
|
||||
|
||||
// u
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.U, true, true, true);
|
||||
assertFalse('IME should not be triggered', mh.waitingForIme_);
|
||||
|
||||
mh.fireEvent('blur', '');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Check attaching to an EventTarget instead of an element.
|
||||
*/
|
||||
function testAttachEventTarget() {
|
||||
var target = new goog.events.EventTarget();
|
||||
|
||||
assertNull(mh.activeElement_);
|
||||
mh.attachInput(target);
|
||||
assertNull(mh.activeElement_);
|
||||
|
||||
mockElement.dispatchEvent(new goog.events.Event('focus', mockElement));
|
||||
assertEquals(mockElement, mh.activeElement_);
|
||||
|
||||
mh.detachInput(target);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure that the active element handling works.
|
||||
*/
|
||||
function testActiveElement() {
|
||||
assertNull(mh.activeElement_);
|
||||
|
||||
mockElement.dispatchEvent('keydown');
|
||||
assertEquals(mockElement, mh.activeElement_);
|
||||
|
||||
mockElement.dispatchEvent('blur');
|
||||
assertNull(mh.activeElement_);
|
||||
|
||||
mockElement.dispatchEvent('focus');
|
||||
assertEquals(mockElement, mh.activeElement_);
|
||||
|
||||
mh.detachInput(mockElement);
|
||||
assertNull(mh.activeElement_);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* We can attach an EventTarget that isn't an element.
|
||||
*/
|
||||
function testAttachEventTarget() {
|
||||
var target = new goog.events.EventTarget();
|
||||
|
||||
assertNull(mh.activeElement_);
|
||||
mh.attachInput(target);
|
||||
assertNull(mh.activeElement_);
|
||||
|
||||
target.dispatchEvent(new goog.events.Event('focus', mockElement));
|
||||
assertEquals(mockElement, mh.activeElement_);
|
||||
|
||||
mh.detachInput(target);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Make sure an already-focused element becomes active immediately.
|
||||
*/
|
||||
function testActiveElementAlreadyFocused() {
|
||||
var element = document.getElementById('textInput');
|
||||
element.style.display = '';
|
||||
element.focus();
|
||||
|
||||
assertNull(mh.activeElement_);
|
||||
|
||||
mh.attachInput(element);
|
||||
assertEquals(element, mh.activeElement_);
|
||||
|
||||
mh.detachInput(element);
|
||||
element.style.display = 'none';
|
||||
}
|
||||
|
||||
function testUpdateDoesNotTriggerSetTokenForSelectRow() {
|
||||
|
||||
var ih = new goog.ui.ac.InputHandler();
|
||||
|
||||
// Set up our input handler with the necessary mocks
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
ih.ac_ = mockAutoCompleter;
|
||||
ih.activeElement_ = mockElement;
|
||||
|
||||
var row = {};
|
||||
ih.selectRow(row, false);
|
||||
|
||||
ih.update();
|
||||
assertFalse('update should not call setToken on selectRow',
|
||||
mockAutoCompleter.setTokenWasCalled);
|
||||
|
||||
ih.update();
|
||||
assertFalse('update should not call setToken on selectRow',
|
||||
mockAutoCompleter.setTokenWasCalled);
|
||||
}
|
||||
|
||||
function testSetTokenText() {
|
||||
|
||||
var ih = new MockInputHandler();
|
||||
|
||||
// Set up our input handler with the necessary mocks
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
ih.ac_ = mockAutoCompleter;
|
||||
ih.activeElement_ = mockElement;
|
||||
mockElement.value = 'bob, wal, joey';
|
||||
ih.setCursorPosition(8);
|
||||
|
||||
ih.setTokenText('waldo', true /* multi-row */);
|
||||
|
||||
assertEquals('bob, waldo, joey', mockElement.value);
|
||||
}
|
||||
|
||||
function testSetTokenTextLeftHandSideOfToken() {
|
||||
|
||||
var ih = new MockInputHandler();
|
||||
ih.setSeparators(' ');
|
||||
ih.setWhitespaceWrapEntries(false);
|
||||
|
||||
// Set up our input handler with the necessary mocks
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
ih.ac_ = mockAutoCompleter;
|
||||
ih.activeElement_ = mockElement;
|
||||
mockElement.value = 'foo bar';
|
||||
// Sets cursor position right before 'bar'
|
||||
ih.setCursorPosition(4);
|
||||
|
||||
ih.setTokenText('bar', true /* multi-row */);
|
||||
|
||||
assertEquals('foo bar ', mockElement.value);
|
||||
}
|
||||
|
||||
function testEmptyTokenWithSeparator() {
|
||||
var ih = new goog.ui.ac.InputHandler();
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
ih.ac_ = mockAutoCompleter;
|
||||
ih.activeElement_ = mockElement;
|
||||
mockElement.value = ', ,';
|
||||
// Sets cursor position before the second comma
|
||||
goog.dom.selection.setStart(mockElement, 2);
|
||||
|
||||
ih.update();
|
||||
assertTrue('update should call setToken on selectRow',
|
||||
mockAutoCompleter.setTokenWasCalled);
|
||||
assertEquals('update should be called with empty string',
|
||||
'', mockAutoCompleter.setToken);
|
||||
}
|
||||
|
||||
function testNonEmptyTokenWithSeparator() {
|
||||
var ih = new goog.ui.ac.InputHandler();
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
ih.ac_ = mockAutoCompleter;
|
||||
ih.activeElement_ = mockElement;
|
||||
mockElement.value = ', joe ,';
|
||||
// Sets cursor position before the second comma
|
||||
goog.dom.selection.setStart(mockElement, 5);
|
||||
|
||||
ih.update();
|
||||
assertTrue('update should call setToken on selectRow',
|
||||
mockAutoCompleter.setTokenWasCalled);
|
||||
assertEquals('update should be called with expected string',
|
||||
'joe', mockAutoCompleter.setToken);
|
||||
}
|
||||
|
||||
function testGetThrottleTime() {
|
||||
var ih = new goog.ui.ac.InputHandler();
|
||||
ih.setThrottleTime(999);
|
||||
assertEquals('throttle time set+get', 999, ih.getThrottleTime());
|
||||
}
|
||||
|
||||
function testGetUpdateDuringTyping() {
|
||||
var ih = new goog.ui.ac.InputHandler();
|
||||
ih.setUpdateDuringTyping(false);
|
||||
assertFalse('update during typing set+get', ih.getUpdateDuringTyping());
|
||||
}
|
||||
|
||||
function testEnterToSelect() {
|
||||
mh.fireEvent('focus', '');
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
|
||||
assertTrue('Should hilite', mh.ac_.selectHilitedWasCalled);
|
||||
assertFalse('Should NOT be dismissed', mh.ac_.dismissWasCalled);
|
||||
}
|
||||
|
||||
function testEnterDoesNotSelectWhenClosed() {
|
||||
mh.fireEvent('focus', '');
|
||||
mh.ac_.isOpen = goog.functions.FALSE;
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.ENTER, true, true, true);
|
||||
assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
|
||||
assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
|
||||
}
|
||||
|
||||
function testTabToSelect() {
|
||||
mh.fireEvent('focus', '');
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true);
|
||||
assertTrue('Should hilite', mh.ac_.selectHilitedWasCalled);
|
||||
assertFalse('Should NOT be dismissed', mh.ac_.dismissWasCalled);
|
||||
}
|
||||
|
||||
function testTabDoesNotSelectWhenClosed() {
|
||||
mh.fireEvent('focus', '');
|
||||
mh.ac_.isOpen = goog.functions.FALSE;
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true);
|
||||
assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
|
||||
assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
|
||||
}
|
||||
|
||||
function testShiftTabDoesNotSelect() {
|
||||
mh.fireEvent('focus', '');
|
||||
mh.ac_.isOpen = goog.functions.TRUE;
|
||||
mh.fireKeyEvents(goog.events.KeyCodes.TAB, true, true, true,
|
||||
{shiftKey: true});
|
||||
assertFalse('Should NOT hilite', mh.ac_.selectHilitedWasCalled);
|
||||
assertTrue('Should be dismissed', mh.ac_.dismissWasCalled);
|
||||
}
|
||||
|
||||
function testEmptySeparatorUsesDefaults() {
|
||||
var inputHandler = new goog.ui.ac.InputHandler('');
|
||||
assertFalse(inputHandler.separatorCheck_.test(''));
|
||||
assertFalse(inputHandler.separatorCheck_.test('x'));
|
||||
assertTrue(inputHandler.separatorCheck_.test(','));
|
||||
}
|
||||
|
||||
function testMultipleSeparatorUsesEmptyDefaults() {
|
||||
var inputHandler = new goog.ui.ac.InputHandler(',\n', null, true);
|
||||
inputHandler.setWhitespaceWrapEntries(false);
|
||||
inputHandler.setSeparators(',\n', '');
|
||||
|
||||
// Set up our input handler with the necessary mocks
|
||||
var mockAutoCompleter = new MockAutoCompleter();
|
||||
inputHandler.ac_ = mockAutoCompleter;
|
||||
inputHandler.activeElement_ = mockElement;
|
||||
mockElement.value = 'bob,wal';
|
||||
inputHandler.setCursorPosition(8);
|
||||
|
||||
inputHandler.setTokenText('waldo', true /* multi-row */);
|
||||
|
||||
assertEquals('bob,waldo', mockElement.value);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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 Factory class to create a simple autocomplete that will match
|
||||
* from an array of data provided via ajax.
|
||||
*
|
||||
* @see ../../demos/autocompleteremote.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.Remote');
|
||||
|
||||
goog.require('goog.ui.ac.AutoComplete');
|
||||
goog.require('goog.ui.ac.InputHandler');
|
||||
goog.require('goog.ui.ac.RemoteArrayMatcher');
|
||||
goog.require('goog.ui.ac.Renderer');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Factory class for building a remote autocomplete widget that autocompletes
|
||||
* an inputbox or text area from a data array provided via ajax.
|
||||
* @param {string} url The Uri which generates the auto complete matches.
|
||||
* @param {Element} input Input element or text area.
|
||||
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
|
||||
* to false.
|
||||
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
|
||||
* "gost" => "ghost".
|
||||
* @constructor
|
||||
* @extends {goog.ui.ac.AutoComplete}
|
||||
*/
|
||||
goog.ui.ac.Remote = function(url, input, opt_multi, opt_useSimilar) {
|
||||
var matcher = new goog.ui.ac.RemoteArrayMatcher(url, !opt_useSimilar);
|
||||
this.matcher_ = matcher;
|
||||
|
||||
var renderer = new goog.ui.ac.Renderer();
|
||||
|
||||
var inputhandler = new goog.ui.ac.InputHandler(null, null, !!opt_multi, 300);
|
||||
|
||||
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
|
||||
|
||||
inputhandler.attachAutoComplete(this);
|
||||
inputhandler.attachInputs(input);
|
||||
};
|
||||
goog.inherits(goog.ui.ac.Remote, goog.ui.ac.AutoComplete);
|
||||
|
||||
|
||||
/**
|
||||
* Set whether or not standard highlighting should be used when rendering rows.
|
||||
* @param {boolean} useStandardHighlighting true if standard highlighting used.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.setUseStandardHighlighting =
|
||||
function(useStandardHighlighting) {
|
||||
this.renderer_.setUseStandardHighlighting(useStandardHighlighting);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Gets the attached InputHandler object.
|
||||
* @return {goog.ui.ac.InputHandler} The input handler.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.getInputHandler = function() {
|
||||
return /** @type {goog.ui.ac.InputHandler} */ (
|
||||
this.selectionHandler_);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the send method ("GET", "POST") for the matcher.
|
||||
* @param {string} method The send method; default: GET.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.setMethod = function(method) {
|
||||
this.matcher_.setMethod(method);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the post data for the matcher.
|
||||
* @param {string} content Post data.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.setContent = function(content) {
|
||||
this.matcher_.setContent(content);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the HTTP headers for the matcher.
|
||||
* @param {Object|goog.structs.Map} headers Map of headers to add to the
|
||||
* request.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.setHeaders = function(headers) {
|
||||
this.matcher_.setHeaders(headers);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the timeout interval for the matcher.
|
||||
* @param {number} interval Number of milliseconds after which an
|
||||
* incomplete request will be aborted; 0 means no timeout is set.
|
||||
*/
|
||||
goog.ui.ac.Remote.prototype.setTimeoutInterval = function(interval) {
|
||||
this.matcher_.setTimeoutInterval(interval);
|
||||
};
|
||||
@@ -0,0 +1,274 @@
|
||||
// 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 Class that retrieves autocomplete matches via an ajax call.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.RemoteArrayMatcher');
|
||||
|
||||
goog.require('goog.Disposable');
|
||||
goog.require('goog.Uri');
|
||||
goog.require('goog.events');
|
||||
goog.require('goog.json');
|
||||
goog.require('goog.net.EventType');
|
||||
goog.require('goog.net.XhrIo');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An array matcher that requests matches via ajax.
|
||||
* @param {string} url The Uri which generates the auto complete matches. The
|
||||
* search term is passed to the server as the 'token' query param.
|
||||
* @param {boolean=} opt_noSimilar If true, request that the server does not do
|
||||
* similarity matches for the input token against the dictionary.
|
||||
* The value is sent to the server as the 'use_similar' query param which is
|
||||
* either "1" (opt_noSimilar==false) or "0" (opt_noSimilar==true).
|
||||
* @param {goog.net.XmlHttpFactory=} opt_xmlHttpFactory Specify the
|
||||
* XmlHttpFactory used to retrieve the matches.
|
||||
* @constructor
|
||||
* @extends {goog.Disposable}
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher =
|
||||
function(url, opt_noSimilar, opt_xmlHttpFactory) {
|
||||
goog.Disposable.call(this);
|
||||
|
||||
/**
|
||||
* The base URL for the ajax call. The token and max_matches are added as
|
||||
* query params.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
this.url_ = url;
|
||||
|
||||
/**
|
||||
* Whether similar matches should be found as well. This is sent as a hint
|
||||
* to the server only.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
this.useSimilar_ = !opt_noSimilar;
|
||||
|
||||
/**
|
||||
* The XhrIo object used for making remote requests. When a new request
|
||||
* is made, the current one is aborted and the new one sent.
|
||||
* @type {goog.net.XhrIo}
|
||||
* @private
|
||||
*/
|
||||
this.xhr_ = new goog.net.XhrIo(opt_xmlHttpFactory);
|
||||
};
|
||||
goog.inherits(goog.ui.ac.RemoteArrayMatcher, goog.Disposable);
|
||||
|
||||
|
||||
/**
|
||||
* The HTTP send method (GET, POST) to use when making the ajax call.
|
||||
* @type {string}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.method_ = 'GET';
|
||||
|
||||
|
||||
/**
|
||||
* Data to submit during a POST.
|
||||
* @type {string|undefined}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.content_ = undefined;
|
||||
|
||||
|
||||
/**
|
||||
* Headers to send with every HTTP request.
|
||||
* @type {Object|goog.structs.Map}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.headers_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Key to the listener on XHR. Used to clear previous listeners.
|
||||
* @type {goog.events.Key}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.lastListenerKey_ = null;
|
||||
|
||||
|
||||
/**
|
||||
* Set the send method ("GET", "POST").
|
||||
* @param {string} method The send method; default: GET.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.setMethod = function(method) {
|
||||
this.method_ = method;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the post data.
|
||||
* @param {string} content Post data.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.setContent = function(content) {
|
||||
this.content_ = content;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the HTTP headers.
|
||||
* @param {Object|goog.structs.Map} headers Map of headers to add to the
|
||||
* request.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.setHeaders = function(headers) {
|
||||
this.headers_ = headers;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Set the timeout interval.
|
||||
* @param {number} interval Number of milliseconds after which an
|
||||
* incomplete request will be aborted; 0 means no timeout is set.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.setTimeoutInterval =
|
||||
function(interval) {
|
||||
this.xhr_.setTimeoutInterval(interval);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Builds a complete GET-style URL, given the base URI and autocomplete related
|
||||
* parameter values.
|
||||
* <b>Override this to build any customized lookup URLs.</b>
|
||||
* <b>Can be used to change request method and any post content as well.</b>
|
||||
* @param {string} uri The base URI of the request target.
|
||||
* @param {string} token Current token in autocomplete.
|
||||
* @param {number} maxMatches Maximum number of matches required.
|
||||
* @param {boolean} useSimilar A hint to the server.
|
||||
* @param {string=} opt_fullString Complete text in the input element.
|
||||
* @return {?string} The complete url. Return null if no request should be sent.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.buildUrl = function(uri,
|
||||
token, maxMatches, useSimilar, opt_fullString) {
|
||||
var url = new goog.Uri(uri);
|
||||
url.setParameterValue('token', token);
|
||||
url.setParameterValue('max_matches', String(maxMatches));
|
||||
url.setParameterValue('use_similar', String(Number(useSimilar)));
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Returns whether the suggestions should be updated?
|
||||
* <b>Override this to prevent updates eg - when token is empty.</b>
|
||||
* @param {string} uri The base URI of the request target.
|
||||
* @param {string} token Current token in autocomplete.
|
||||
* @param {number} maxMatches Maximum number of matches required.
|
||||
* @param {boolean} useSimilar A hint to the server.
|
||||
* @param {string=} opt_fullString Complete text in the input element.
|
||||
* @return {boolean} Whether new matches be requested.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.shouldRequestMatches =
|
||||
function(uri, token, maxMatches, useSimilar, opt_fullString) {
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Parses and retrieves the array of suggestions from XHR response.
|
||||
* <b>Override this if the response is not a simple JSON array.</b>
|
||||
* @param {string} responseText The XHR response text.
|
||||
* @return {Array<string>} The array of suggestions.
|
||||
* @protected
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.parseResponseText = function(
|
||||
responseText) {
|
||||
|
||||
var matches = [];
|
||||
// If there is no response text, unsafeParse will throw a syntax error.
|
||||
if (responseText) {
|
||||
/** @preserveTry */
|
||||
try {
|
||||
matches = goog.json.unsafeParse(responseText);
|
||||
} catch (exception) {
|
||||
}
|
||||
}
|
||||
return /** @type {Array<string>} */ (matches);
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Handles the XHR response.
|
||||
* @param {string} token The XHR autocomplete token.
|
||||
* @param {Function} matchHandler The AutoComplete match handler.
|
||||
* @param {goog.events.Event} event The XHR success event.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.xhrCallback = function(token,
|
||||
matchHandler, event) {
|
||||
var text = event.target.getResponseText();
|
||||
matchHandler(token, this.parseResponseText(text));
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a set of matching rows from the server via ajax.
|
||||
* @param {string} token The text that should be matched; passed to the server
|
||||
* as the 'token' query param.
|
||||
* @param {number} maxMatches The maximum number of matches requested from the
|
||||
* server; passed as the 'max_matches' query param. The server is
|
||||
* responsible for limiting the number of matches that are returned.
|
||||
* @param {Function} matchHandler Callback to execute on the result after
|
||||
* matching.
|
||||
* @param {string=} opt_fullString The full string from the input box.
|
||||
*/
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.requestMatchingRows =
|
||||
function(token, maxMatches, matchHandler, opt_fullString) {
|
||||
|
||||
if (!this.shouldRequestMatches(this.url_, token, maxMatches, this.useSimilar_,
|
||||
opt_fullString)) {
|
||||
return;
|
||||
}
|
||||
// Set the query params on the URL.
|
||||
var url = this.buildUrl(this.url_, token, maxMatches, this.useSimilar_,
|
||||
opt_fullString);
|
||||
if (!url) {
|
||||
// Do nothing if there is no URL.
|
||||
return;
|
||||
}
|
||||
|
||||
// The callback evals the server response and calls the match handler on
|
||||
// the array of matches.
|
||||
var callback = goog.bind(this.xhrCallback, this, token, matchHandler);
|
||||
|
||||
// Abort the current request and issue the new one; prevent requests from
|
||||
// being queued up by the browser with a slow server
|
||||
if (this.xhr_.isActive()) {
|
||||
this.xhr_.abort();
|
||||
}
|
||||
// This ensures if previous XHR is aborted or ends with error, the
|
||||
// corresponding success-callbacks are cleared.
|
||||
if (this.lastListenerKey_) {
|
||||
goog.events.unlistenByKey(this.lastListenerKey_);
|
||||
}
|
||||
// Listen once ensures successful callback gets cleared by itself.
|
||||
this.lastListenerKey_ = goog.events.listenOnce(this.xhr_,
|
||||
goog.net.EventType.SUCCESS, callback);
|
||||
this.xhr_.send(url, this.method_, this.content_, this.headers_);
|
||||
};
|
||||
|
||||
|
||||
/** @override */
|
||||
goog.ui.ac.RemoteArrayMatcher.prototype.disposeInternal = function() {
|
||||
this.xhr_.dispose();
|
||||
goog.ui.ac.RemoteArrayMatcher.superClass_.disposeInternal.call(
|
||||
this);
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2009 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<!--
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.RemoteArrayMatcher
|
||||
</title>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.RemoteArrayMatcherTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2009 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.
|
||||
|
||||
goog.provide('goog.ui.ac.RemoteArrayMatcherTest');
|
||||
goog.setTestOnly('goog.ui.ac.RemoteArrayMatcherTest');
|
||||
|
||||
goog.require('goog.json');
|
||||
goog.require('goog.net.XhrIo');
|
||||
goog.require('goog.testing.MockControl');
|
||||
goog.require('goog.testing.jsunit');
|
||||
goog.require('goog.testing.net.XhrIo');
|
||||
goog.require('goog.ui.ac.RemoteArrayMatcher');
|
||||
var url = 'http://www.google.com';
|
||||
var token = 'goog';
|
||||
var maxMatches = 5;
|
||||
var fullToken = 'google';
|
||||
|
||||
var responseJsonText = "['eric', 'larry', 'sergey', 'marissa', 'pupius']";
|
||||
var responseJson = goog.json.unsafeParse(responseJsonText);
|
||||
|
||||
var mockControl;
|
||||
var mockMatchHandler;
|
||||
|
||||
|
||||
function setUp() {
|
||||
goog.net.XhrIo = goog.testing.net.XhrIo;
|
||||
mockControl = new goog.testing.MockControl();
|
||||
mockMatchHandler = mockControl.createFunctionMock();
|
||||
}
|
||||
|
||||
function testRequestMatchingRows_noSimilarTrue() {
|
||||
var matcher = new goog.ui.ac.RemoteArrayMatcher(url);
|
||||
mockMatchHandler(token, responseJson);
|
||||
mockControl.$replayAll();
|
||||
matcher.requestMatchingRows(token, maxMatches, mockMatchHandler, fullToken);
|
||||
matcher.xhr_.simulateResponse(200, responseJsonText);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
|
||||
function testRequestMatchingRows_twoCalls() {
|
||||
var matcher = new goog.ui.ac.RemoteArrayMatcher(url);
|
||||
|
||||
var dummyMatchHandler = mockControl.createFunctionMock();
|
||||
|
||||
mockMatchHandler(token, responseJson);
|
||||
mockControl.$replayAll();
|
||||
|
||||
matcher.requestMatchingRows(token, maxMatches, dummyMatchHandler,
|
||||
fullToken);
|
||||
|
||||
matcher.requestMatchingRows(token, maxMatches, mockMatchHandler, fullToken);
|
||||
matcher.xhr_.simulateResponse(200, responseJsonText);
|
||||
mockControl.$verifyAll();
|
||||
mockControl.$resetAll();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<!--
|
||||
Copyright 2010 The Closure Library Authors. All Rights Reserved.
|
||||
|
||||
Use of this source code is governed by the Apache License, Version 2.0.
|
||||
See the COPYING file for details.
|
||||
-->
|
||||
<!--
|
||||
-->
|
||||
<head>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<title>
|
||||
Closure Unit Tests - goog.ui.ac.Renderer
|
||||
</title>
|
||||
<style type="text/css">
|
||||
#viewport {
|
||||
width: 400px;
|
||||
height: 200px;
|
||||
overflow: hidden; /* Suppress scroll bars to get consistent cross-browser resizing */
|
||||
position: relative;
|
||||
}
|
||||
</style>
|
||||
<script src="../../base.js">
|
||||
</script>
|
||||
<script>
|
||||
goog.require('goog.ui.ac.RendererTest');
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="someElement">Click target</div>
|
||||
<div id="target">Target</div>
|
||||
<div id="viewport">
|
||||
Parent (viewport) element for some tests
|
||||
<div id="viewportTarget">Target for viewport tests</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,80 @@
|
||||
// Copyright 2012 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 Options for rendering matches.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.RenderOptions');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* A simple class that contains options for rendering a set of autocomplete
|
||||
* matches. Used as an optional argument in the callback from the matcher.
|
||||
* @constructor
|
||||
*/
|
||||
goog.ui.ac.RenderOptions = function() {
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Whether the current highlighting is to be preserved when displaying the new
|
||||
* set of matches.
|
||||
* @type {boolean}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.preserveHilited_ = false;
|
||||
|
||||
|
||||
/**
|
||||
* Whether the first match is to be highlighted. When undefined the autoHilite
|
||||
* flag of the autocomplete is used.
|
||||
* @type {boolean|undefined}
|
||||
* @private
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.autoHilite_;
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} flag The new value for the preserveHilited_ flag.
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.setPreserveHilited = function(flag) {
|
||||
this.preserveHilited_ = flag;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean} The value of the preserveHilited_ flag.
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.getPreserveHilited = function() {
|
||||
return this.preserveHilited_;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @param {boolean} flag The new value for the autoHilite_ flag.
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.setAutoHilite = function(flag) {
|
||||
this.autoHilite_ = flag;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @return {boolean|undefined} The value of the autoHilite_ flag.
|
||||
*/
|
||||
goog.ui.ac.RenderOptions.prototype.getAutoHilite = function() {
|
||||
return this.autoHilite_;
|
||||
};
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 Class for managing the interactions between a rich autocomplete
|
||||
* object and a text-input or textarea.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.RichInputHandler');
|
||||
|
||||
goog.require('goog.ui.ac.InputHandler');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Class for managing the interaction between an autocomplete object and a
|
||||
* text-input or textarea.
|
||||
* @param {?string=} opt_separators Seperators to split multiple entries.
|
||||
* @param {?string=} opt_literals Characters used to delimit text literals.
|
||||
* @param {?boolean=} opt_multi Whether to allow multiple entries
|
||||
* (Default: true).
|
||||
* @param {?number=} opt_throttleTime Number of milliseconds to throttle
|
||||
* keyevents with (Default: 150).
|
||||
* @constructor
|
||||
* @extends {goog.ui.ac.InputHandler}
|
||||
*/
|
||||
goog.ui.ac.RichInputHandler = function(opt_separators, opt_literals,
|
||||
opt_multi, opt_throttleTime) {
|
||||
goog.ui.ac.InputHandler.call(this, opt_separators, opt_literals,
|
||||
opt_multi, opt_throttleTime);
|
||||
};
|
||||
goog.inherits(goog.ui.ac.RichInputHandler, goog.ui.ac.InputHandler);
|
||||
|
||||
|
||||
/**
|
||||
* Selects the given rich row. The row's select(target) method is called.
|
||||
* @param {Object} row The row to select.
|
||||
* @return {boolean} Whether to suppress the update event.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ac.RichInputHandler.prototype.selectRow = function(row) {
|
||||
var suppressUpdate = goog.ui.ac.RichInputHandler.superClass_
|
||||
.selectRow.call(this, row);
|
||||
row.select(this.ac_.getTarget());
|
||||
return suppressUpdate;
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
// 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 Factory class to create a rich autocomplete that will match
|
||||
* from an array of data provided via ajax. The server returns a complex data
|
||||
* structure that is used with client-side javascript functions to render the
|
||||
* results.
|
||||
*
|
||||
* The server sends a list of the form:
|
||||
* [["type1", {...}, {...}, ...], ["type2", {...}, {...}, ...], ...]
|
||||
* The first element of each sublist is a string designating the type of the
|
||||
* hashes in the sublist, each of which represents one match. The type string
|
||||
* must be the name of a function(item) which converts the hash into a rich
|
||||
* row that contains both a render(node, token) and a select(target) method.
|
||||
* The render method is called by the renderer when rendering the rich row,
|
||||
* and the select method is called by the RichInputHandler when the rich row is
|
||||
* selected.
|
||||
*
|
||||
* @see ../../demos/autocompleterichremote.html
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.RichRemote');
|
||||
|
||||
goog.require('goog.ui.ac.AutoComplete');
|
||||
goog.require('goog.ui.ac.Remote');
|
||||
goog.require('goog.ui.ac.Renderer');
|
||||
goog.require('goog.ui.ac.RichInputHandler');
|
||||
goog.require('goog.ui.ac.RichRemoteArrayMatcher');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Factory class to create a rich autocomplete widget that autocompletes an
|
||||
* inputbox or textarea from data provided via ajax. The server returns a
|
||||
* complex data structure that is used with client-side javascript functions to
|
||||
* render the results.
|
||||
* @param {string} url The Uri which generates the auto complete matches.
|
||||
* @param {Element} input Input element or text area.
|
||||
* @param {boolean=} opt_multi Whether to allow multiple entries; defaults
|
||||
* to false.
|
||||
* @param {boolean=} opt_useSimilar Whether to use similar matches; e.g.
|
||||
* "gost" => "ghost".
|
||||
* @constructor
|
||||
* @extends {goog.ui.ac.Remote}
|
||||
*/
|
||||
goog.ui.ac.RichRemote = function(url, input, opt_multi, opt_useSimilar) {
|
||||
// Create a custom renderer that renders rich rows. The renderer calls
|
||||
// row.render(node, token) for each row.
|
||||
var customRenderer = {};
|
||||
customRenderer.renderRow = function(row, token, node) {
|
||||
return row.data.render(node, token);
|
||||
};
|
||||
|
||||
/**
|
||||
* A standard renderer that uses a custom row renderer to display the
|
||||
* rich rows generated by this autocomplete widget.
|
||||
* @type {goog.ui.ac.Renderer}
|
||||
* @private
|
||||
*/
|
||||
var renderer = new goog.ui.ac.Renderer(null, customRenderer);
|
||||
this.renderer_ = renderer;
|
||||
|
||||
/**
|
||||
* A remote matcher that parses rich results returned by the server.
|
||||
* @type {goog.ui.ac.RichRemoteArrayMatcher}
|
||||
* @private
|
||||
*/
|
||||
var matcher = new goog.ui.ac.RichRemoteArrayMatcher(url,
|
||||
!opt_useSimilar);
|
||||
this.matcher_ = matcher;
|
||||
|
||||
/**
|
||||
* An input handler that calls select on a row when it is selected.
|
||||
* @type {goog.ui.ac.RichInputHandler}
|
||||
* @private
|
||||
*/
|
||||
var inputhandler = new goog.ui.ac.RichInputHandler(null, null,
|
||||
!!opt_multi, 300);
|
||||
|
||||
// Create the widget and connect it to the input handler.
|
||||
goog.ui.ac.AutoComplete.call(this, matcher, renderer, inputhandler);
|
||||
inputhandler.attachAutoComplete(this);
|
||||
inputhandler.attachInputs(input);
|
||||
};
|
||||
goog.inherits(goog.ui.ac.RichRemote, goog.ui.ac.Remote);
|
||||
|
||||
|
||||
/**
|
||||
* Set the filter that is called before the array matches are returned.
|
||||
* @param {Function} rowFilter A function(rows) that returns an array of rows as
|
||||
* a subset of the rows input array.
|
||||
*/
|
||||
goog.ui.ac.RichRemote.prototype.setRowFilter = function(rowFilter) {
|
||||
this.matcher_.setRowFilter(rowFilter);
|
||||
};
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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 Class that retrieves rich autocomplete matches, represented as
|
||||
* a structured list of lists, via an ajax call. The first element of each
|
||||
* sublist is the name of a client-side javascript function that converts the
|
||||
* remaining sublist elements into rich rows.
|
||||
*
|
||||
*/
|
||||
|
||||
goog.provide('goog.ui.ac.RichRemoteArrayMatcher');
|
||||
|
||||
goog.require('goog.json');
|
||||
goog.require('goog.ui.ac.RemoteArrayMatcher');
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* An array matcher that requests rich matches via ajax and converts them into
|
||||
* rich rows.
|
||||
* @param {string} url The Uri which generates the auto complete matches. The
|
||||
* search term is passed to the server as the 'token' query param.
|
||||
* @param {boolean=} opt_noSimilar If true, request that the server does not do
|
||||
* similarity matches for the input token against the dictionary.
|
||||
* The value is sent to the server as the 'use_similar' query param which is
|
||||
* either "1" (opt_noSimilar==false) or "0" (opt_noSimilar==true).
|
||||
* @constructor
|
||||
* @extends {goog.ui.ac.RemoteArrayMatcher}
|
||||
*/
|
||||
goog.ui.ac.RichRemoteArrayMatcher = function(url, opt_noSimilar) {
|
||||
goog.ui.ac.RemoteArrayMatcher.call(this, url, opt_noSimilar);
|
||||
|
||||
/**
|
||||
* A function(rows) that is called before the array matches are returned.
|
||||
* It runs client-side and filters the results given by the server before
|
||||
* being rendered by the client.
|
||||
* @type {Function}
|
||||
* @private
|
||||
*/
|
||||
this.rowFilter_ = null;
|
||||
|
||||
};
|
||||
goog.inherits(goog.ui.ac.RichRemoteArrayMatcher, goog.ui.ac.RemoteArrayMatcher);
|
||||
|
||||
|
||||
/**
|
||||
* Set the filter that is called before the array matches are returned.
|
||||
* @param {Function} rowFilter A function(rows) that returns an array of rows as
|
||||
* a subset of the rows input array.
|
||||
*/
|
||||
goog.ui.ac.RichRemoteArrayMatcher.prototype.setRowFilter = function(rowFilter) {
|
||||
this.rowFilter_ = rowFilter;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Retrieve a set of matching rows from the server via ajax and convert them
|
||||
* into rich rows.
|
||||
* @param {string} token The text that should be matched; passed to the server
|
||||
* as the 'token' query param.
|
||||
* @param {number} maxMatches The maximum number of matches requested from the
|
||||
* server; passed as the 'max_matches' query param. The server is
|
||||
* responsible for limiting the number of matches that are returned.
|
||||
* @param {Function} matchHandler Callback to execute on the result after
|
||||
* matching.
|
||||
* @override
|
||||
*/
|
||||
goog.ui.ac.RichRemoteArrayMatcher.prototype.requestMatchingRows =
|
||||
function(token, maxMatches, matchHandler) {
|
||||
// The RichRemoteArrayMatcher must map over the results and filter them
|
||||
// before calling the request matchHandler. This is done by passing
|
||||
// myMatchHandler to RemoteArrayMatcher.requestMatchingRows which maps,
|
||||
// filters, and then calls matchHandler.
|
||||
var myMatchHandler = goog.bind(function(token, matches) {
|
||||
/** @preserveTry */
|
||||
try {
|
||||
var rows = [];
|
||||
for (var i = 0; i < matches.length; i++) {
|
||||
var func = /** @type {!Function} */
|
||||
(goog.json.unsafeParse(matches[i][0]));
|
||||
for (var j = 1; j < matches[i].length; j++) {
|
||||
var richRow = func(matches[i][j]);
|
||||
rows.push(richRow);
|
||||
|
||||
// If no render function was provided, set the node's innerHTML.
|
||||
if (typeof richRow.render == 'undefined') {
|
||||
richRow.render = function(node, token) {
|
||||
node.innerHTML = richRow.toString();
|
||||
};
|
||||
}
|
||||
|
||||
// If no select function was provided, set the text of the input.
|
||||
if (typeof richRow.select == 'undefined') {
|
||||
richRow.select = function(target) {
|
||||
target.value = richRow.toString();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.rowFilter_) {
|
||||
rows = this.rowFilter_(rows);
|
||||
}
|
||||
matchHandler(token, rows);
|
||||
} catch (exception) {
|
||||
// TODO(user): Is this what we want?
|
||||
matchHandler(token, []);
|
||||
}
|
||||
}, this);
|
||||
|
||||
// Call the super's requestMatchingRows with myMatchHandler
|
||||
goog.ui.ac.RichRemoteArrayMatcher.superClass_
|
||||
.requestMatchingRows.call(this, token, maxMatches, myMatchHandler);
|
||||
};
|
||||
Reference in New Issue
Block a user